@genome-spy/core 0.66.1 → 0.68.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 (191) hide show
  1. package/dist/bundle/index.es.js +7669 -6115
  2. package/dist/bundle/index.js +114 -133
  3. package/dist/schema.json +534 -132
  4. package/dist/src/data/collector.d.ts +20 -0
  5. package/dist/src/data/collector.d.ts.map +1 -1
  6. package/dist/src/data/collector.js +148 -0
  7. package/dist/src/data/dataFlow.d.ts +6 -0
  8. package/dist/src/data/dataFlow.d.ts.map +1 -1
  9. package/dist/src/data/dataFlow.js +10 -0
  10. package/dist/src/data/flowHandle.d.ts +2 -0
  11. package/dist/src/data/flowHandle.d.ts.map +1 -1
  12. package/dist/src/data/flowHandle.js +1 -0
  13. package/dist/src/data/flowInit.d.ts +12 -4
  14. package/dist/src/data/flowInit.d.ts.map +1 -1
  15. package/dist/src/data/flowInit.js +115 -17
  16. package/dist/src/data/flowNode.d.ts +8 -0
  17. package/dist/src/data/flowNode.d.ts.map +1 -1
  18. package/dist/src/data/flowNode.js +18 -0
  19. package/dist/src/data/keyIndex.d.ts +18 -0
  20. package/dist/src/data/keyIndex.d.ts.map +1 -0
  21. package/dist/src/data/keyIndex.js +241 -0
  22. package/dist/src/data/keyIndex.test.d.ts +2 -0
  23. package/dist/src/data/keyIndex.test.d.ts.map +1 -0
  24. package/dist/src/data/sources/dataSource.d.ts.map +1 -1
  25. package/dist/src/data/sources/dataSource.js +5 -1
  26. package/dist/src/data/sources/dataSourceFactory.d.ts +14 -12
  27. package/dist/src/data/sources/dataSourceFactory.d.ts.map +1 -1
  28. package/dist/src/data/sources/dataSourceFactory.js +52 -16
  29. package/dist/src/data/sources/lazy/mockLazySource.d.ts +29 -0
  30. package/dist/src/data/sources/lazy/mockLazySource.d.ts.map +1 -0
  31. package/dist/src/data/sources/lazy/mockLazySource.js +44 -0
  32. package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts +22 -1
  33. package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts.map +1 -1
  34. package/dist/src/data/sources/lazy/singleAxisLazySource.js +34 -2
  35. package/dist/src/data/sources/lazy/singleAxisWindowedSource.d.ts.map +1 -1
  36. package/dist/src/data/sources/lazy/singleAxisWindowedSource.js +15 -0
  37. package/dist/src/data/sources/lazy/tabixSource.d.ts.map +1 -1
  38. package/dist/src/data/sources/lazy/tabixSource.js +15 -5
  39. package/dist/src/data/transforms/stack.d.ts.map +1 -1
  40. package/dist/src/data/transforms/stack.js +1 -0
  41. package/dist/src/encoder/accessor.d.ts +43 -0
  42. package/dist/src/encoder/accessor.d.ts.map +1 -1
  43. package/dist/src/encoder/accessor.js +164 -0
  44. package/dist/src/encoder/encoder.d.ts +11 -2
  45. package/dist/src/encoder/encoder.d.ts.map +1 -1
  46. package/dist/src/encoder/encoder.js +24 -4
  47. package/dist/src/encoder/metadataChannels.d.ts +15 -0
  48. package/dist/src/encoder/metadataChannels.d.ts.map +1 -0
  49. package/dist/src/encoder/metadataChannels.js +65 -0
  50. package/dist/src/encoder/metadataChannels.test.d.ts +2 -0
  51. package/dist/src/encoder/metadataChannels.test.d.ts.map +1 -0
  52. package/dist/src/genome/scaleLocus.d.ts.map +1 -1
  53. package/dist/src/genome/scaleLocus.js +14 -1
  54. package/dist/src/genomeSpy/containerUi.d.ts +0 -1
  55. package/dist/src/genomeSpy/containerUi.d.ts.map +1 -1
  56. package/dist/src/genomeSpy/containerUi.js +0 -14
  57. package/dist/src/genomeSpy/loadingIndicatorManager.d.ts +3 -7
  58. package/dist/src/genomeSpy/loadingIndicatorManager.d.ts.map +1 -1
  59. package/dist/src/genomeSpy/loadingIndicatorManager.js +68 -20
  60. package/dist/src/genomeSpy/loadingStatusRegistry.d.ts +52 -0
  61. package/dist/src/genomeSpy/loadingStatusRegistry.d.ts.map +1 -0
  62. package/dist/src/genomeSpy/loadingStatusRegistry.js +86 -0
  63. package/dist/src/genomeSpy/viewContextFactory.d.ts.map +1 -1
  64. package/dist/src/genomeSpy/viewContextFactory.js +0 -1
  65. package/dist/src/genomeSpy/viewDataInit.d.ts +10 -0
  66. package/dist/src/genomeSpy/viewDataInit.d.ts.map +1 -1
  67. package/dist/src/genomeSpy/viewDataInit.js +166 -2
  68. package/dist/src/genomeSpy/viewDataInit.test.d.ts +2 -0
  69. package/dist/src/genomeSpy/viewDataInit.test.d.ts.map +1 -0
  70. package/dist/src/genomeSpy.d.ts +1 -2
  71. package/dist/src/genomeSpy.d.ts.map +1 -1
  72. package/dist/src/genomeSpy.js +69 -27
  73. package/dist/src/gl/dataToVertices.d.ts.map +1 -1
  74. package/dist/src/gl/dataToVertices.js +16 -4
  75. package/dist/src/marks/mark.d.ts.map +1 -1
  76. package/dist/src/marks/mark.js +18 -11
  77. package/dist/src/marks/markUtils.js +1 -1
  78. package/dist/src/scale/scale.d.ts +6 -1
  79. package/dist/src/scale/scale.d.ts.map +1 -1
  80. package/dist/src/scale/scale.js +83 -23
  81. package/dist/src/scales/axisResolution.d.ts.map +1 -1
  82. package/dist/src/scales/axisResolution.js +10 -0
  83. package/dist/src/scales/{scaleDomainAggregator.d.ts → domainPlanner.d.ts} +8 -5
  84. package/dist/src/scales/domainPlanner.d.ts.map +1 -0
  85. package/dist/src/scales/domainPlanner.js +285 -0
  86. package/dist/src/scales/domainPlanner.test.d.ts +2 -0
  87. package/dist/src/scales/domainPlanner.test.d.ts.map +1 -0
  88. package/dist/src/scales/scaleInstanceManager.d.ts.map +1 -1
  89. package/dist/src/scales/scaleInstanceManager.js +8 -4
  90. package/dist/src/scales/scaleInteractionController.d.ts +6 -0
  91. package/dist/src/scales/scaleInteractionController.d.ts.map +1 -1
  92. package/dist/src/scales/scaleInteractionController.js +41 -3
  93. package/dist/src/scales/scaleResolution.d.ts +19 -16
  94. package/dist/src/scales/scaleResolution.d.ts.map +1 -1
  95. package/dist/src/scales/scaleResolution.js +255 -70
  96. package/dist/src/scales/scaleResolution.test.d.ts.map +1 -1
  97. package/dist/src/selection/selection.d.ts +21 -0
  98. package/dist/src/selection/selection.d.ts.map +1 -1
  99. package/dist/src/selection/selection.js +82 -0
  100. package/dist/src/spec/channel.d.ts +52 -15
  101. package/dist/src/spec/data.d.ts +4 -0
  102. package/dist/src/spec/parameter.d.ts +16 -11
  103. package/dist/src/spec/testing.d.ts +12 -0
  104. package/dist/src/spec/testing.d.ts.map +1 -0
  105. package/dist/src/spec/testing.js +20 -0
  106. package/dist/src/spec/view.d.ts +45 -10
  107. package/dist/src/styles/genome-spy.css +3 -31
  108. package/dist/src/styles/genome-spy.css.d.ts +1 -1
  109. package/dist/src/styles/genome-spy.css.d.ts.map +1 -1
  110. package/dist/src/styles/genome-spy.css.js +0 -29
  111. package/dist/src/types/encoder.d.ts +37 -2
  112. package/dist/src/types/rendering.d.ts +4 -3
  113. package/dist/src/types/viewContext.d.ts +0 -14
  114. package/dist/src/utils/domainArray.d.ts.map +1 -1
  115. package/dist/src/utils/domainArray.js +3 -0
  116. package/dist/src/utils/indexer.d.ts +3 -0
  117. package/dist/src/utils/indexer.d.ts.map +1 -1
  118. package/dist/src/utils/indexer.js +3 -0
  119. package/dist/src/utils/throttle.d.ts +4 -1
  120. package/dist/src/utils/throttle.d.ts.map +1 -1
  121. package/dist/src/utils/throttle.js +54 -23
  122. package/dist/src/utils/throttle.test.d.ts +2 -0
  123. package/dist/src/utils/throttle.test.d.ts.map +1 -0
  124. package/dist/src/utils/transition.d.ts +21 -0
  125. package/dist/src/utils/transition.d.ts.map +1 -1
  126. package/dist/src/utils/transition.js +28 -0
  127. package/dist/src/utils/ui/tooltip.d.ts.map +1 -1
  128. package/dist/src/utils/ui/tooltip.js +7 -1
  129. package/dist/src/utils/ui/tooltip.test.d.ts +2 -0
  130. package/dist/src/utils/ui/tooltip.test.d.ts.map +1 -0
  131. package/dist/src/view/axisGridView.d.ts.map +1 -1
  132. package/dist/src/view/axisGridView.js +22 -5
  133. package/dist/src/view/axisView.d.ts.map +1 -1
  134. package/dist/src/view/axisView.js +20 -5
  135. package/dist/src/view/concatView.js +3 -3
  136. package/dist/src/view/containerMutationHelper.d.ts.map +1 -1
  137. package/dist/src/view/containerMutationHelper.js +6 -2
  138. package/dist/src/view/containerView.d.ts +9 -5
  139. package/dist/src/view/containerView.d.ts.map +1 -1
  140. package/dist/src/view/containerView.js +34 -9
  141. package/dist/src/view/dataReadiness.d.ts +46 -0
  142. package/dist/src/view/dataReadiness.d.ts.map +1 -0
  143. package/dist/src/view/dataReadiness.js +267 -0
  144. package/dist/src/view/dataReadiness.test.d.ts +2 -0
  145. package/dist/src/view/dataReadiness.test.d.ts.map +1 -0
  146. package/dist/src/view/facetView.d.ts.map +1 -1
  147. package/dist/src/view/facetView.js +7 -5
  148. package/dist/src/view/flowBuilder.d.ts +5 -3
  149. package/dist/src/view/flowBuilder.d.ts.map +1 -1
  150. package/dist/src/view/flowBuilder.js +74 -7
  151. package/dist/src/view/gridView/gridChild.d.ts.map +1 -1
  152. package/dist/src/view/gridView/gridChild.js +8 -0
  153. package/dist/src/view/gridView/gridView.d.ts.map +1 -1
  154. package/dist/src/view/gridView/gridView.js +119 -2
  155. package/dist/src/view/gridView/scrollbar.d.ts.map +1 -1
  156. package/dist/src/view/gridView/scrollbar.js +3 -0
  157. package/dist/src/view/gridView/selectionRect.d.ts.map +1 -1
  158. package/dist/src/view/gridView/selectionRect.js +20 -5
  159. package/dist/src/view/gridView/separatorView.d.ts +51 -0
  160. package/dist/src/view/gridView/separatorView.d.ts.map +1 -0
  161. package/dist/src/view/gridView/separatorView.js +275 -0
  162. package/dist/src/view/layerView.js +3 -3
  163. package/dist/src/view/layout/flexLayout.d.ts +0 -30
  164. package/dist/src/view/layout/flexLayout.d.ts.map +1 -1
  165. package/dist/src/view/layout/flexLayout.js +0 -86
  166. package/dist/src/view/paramMediator.d.ts +19 -0
  167. package/dist/src/view/paramMediator.d.ts.map +1 -1
  168. package/dist/src/view/paramMediator.js +86 -19
  169. package/dist/src/view/testUtils.d.ts.map +1 -1
  170. package/dist/src/view/testUtils.js +11 -1
  171. package/dist/src/view/unitView.d.ts +8 -13
  172. package/dist/src/view/unitView.d.ts.map +1 -1
  173. package/dist/src/view/unitView.js +127 -43
  174. package/dist/src/view/view.d.ts +34 -14
  175. package/dist/src/view/view.d.ts.map +1 -1
  176. package/dist/src/view/view.js +119 -9
  177. package/dist/src/view/viewFactory.d.ts.map +1 -1
  178. package/dist/src/view/viewFactory.js +20 -1
  179. package/dist/src/view/viewSelectors.d.ts +148 -0
  180. package/dist/src/view/viewSelectors.d.ts.map +1 -0
  181. package/dist/src/view/viewSelectors.js +773 -0
  182. package/dist/src/view/viewSelectors.test.d.ts +2 -0
  183. package/dist/src/view/viewSelectors.test.d.ts.map +1 -0
  184. package/dist/src/view/viewUtils.d.ts +0 -8
  185. package/dist/src/view/viewUtils.d.ts.map +1 -1
  186. package/dist/src/view/viewUtils.js +1 -21
  187. package/package.json +3 -3
  188. package/dist/src/scales/scaleDomainAggregator.d.ts.map +0 -1
  189. package/dist/src/scales/scaleDomainAggregator.js +0 -162
  190. package/dist/src/scales/scaleDomainAggregator.test.d.ts +0 -2
  191. package/dist/src/scales/scaleDomainAggregator.test.d.ts.map +0 -1
@@ -13,7 +13,7 @@ import { configureDomain } from "../scale/scale.js";
13
13
 
14
14
  import ScaleInstanceManager from "./scaleInstanceManager.js";
15
15
  import { resolveScalePropsBase } from "./scalePropsResolver.js";
16
- import ScaleDomainAggregator from "./scaleDomainAggregator.js";
16
+ import DomainPlanner from "./domainPlanner.js";
17
17
  import ScaleInteractionController from "./scaleInteractionController.js";
18
18
  import {
19
19
  INDEX,
@@ -23,9 +23,11 @@ import {
23
23
  QUANTITATIVE,
24
24
  } from "./scaleResolutionConstants.js";
25
25
 
26
+ import { getAccessorDomainKey } from "../encoder/accessor.js";
26
27
  import { isSecondaryChannel } from "../encoder/encoder.js";
27
28
  import { NominalDomain } from "../utils/domainArray.js";
28
- import { asArray, shallowArrayEquals } from "../utils/arrayUtils.js";
29
+ import { shallowArrayEquals } from "../utils/arrayUtils.js";
30
+ import createIndexer from "../utils/indexer.js";
29
31
 
30
32
  // Register scaleLocus to Vega-Scale.
31
33
  // Loci are discrete but the scale's domain can be adjusted in a continuous manner.
@@ -42,7 +44,7 @@ export { INDEX, LOCUS, NOMINAL, ORDINAL, QUANTITATIVE };
42
44
  * @prop {import("../view/unitView.js").default} view TODO: Get rid of the view reference
43
45
  * @prop {T} channel
44
46
  * @prop {import("../spec/channel.js").ChannelDefWithScale} channelDef
45
- * @prop {(channel: ChannelWithScale, type: import("../spec/channel.js").Type) => DomainArray} dataDomainSource
47
+ * @prop {boolean} contributesToDomain
46
48
  */
47
49
  /**
48
50
  * Resolves a shared scale for a channel by merging scale properties and domains
@@ -51,6 +53,16 @@ export { INDEX, LOCUS, NOMINAL, ORDINAL, QUANTITATIVE };
51
53
  * notifications, while delegating domain aggregation, scale instance setup, and
52
54
  * interaction logic to focused helpers.
53
55
  *
56
+ * Documentation overview of current concerns this class (and its helpers) deal with:
57
+ * - Resolution membership and rules (shared/independent/forced/excluded, visibility, registration).
58
+ * - Scale property aggregation (merge props, channel overrides, unique scale names).
59
+ * - Domain computation and caching (configured/data unions, defaults, indexer stability, subscriptions).
60
+ * - Scale instance lifecycle (create, reconfigure props, apply domains, notify changes).
61
+ * - Interaction and zoom (zoom/pan/reset coordination, snapshots, zoom extents).
62
+ * - Rendering integration (range textures, axis sizing/positioning).
63
+ * - Locus-specific conversions (complex intervals, genome extent bindings).
64
+ * - Diagnostics and edge cases (ordinal unknown, nice/zero/padding, log warnings).
65
+ *
54
66
  * @implements {ScaleResolutionApi}
55
67
  */
56
68
  export default class ScaleResolution {
@@ -74,6 +86,9 @@ export default class ScaleResolution {
74
86
  /** @type {Set<ScaleResolutionMember>} The involved views */
75
87
  #members = new Set();
76
88
 
89
+ /** @type {Set<ScaleResolutionMember>} */
90
+ #dataDomainMembers = new Set();
91
+
77
92
  /**
78
93
  * @type {Record<ScaleResolutionEventType, Set<ScaleResolutionListener>>}
79
94
  */
@@ -85,12 +100,17 @@ export default class ScaleResolution {
85
100
  /** @type {ScaleInstanceManager} */
86
101
  #scaleManager;
87
102
 
88
- /** @type {ScaleDomainAggregator} */
103
+ /** @type {DomainPlanner} */
89
104
  #domainAggregator;
90
105
 
91
106
  /** @type {ScaleInteractionController} */
92
107
  #interactionController;
93
108
 
109
+ /** @type {ReturnType<typeof createIndexer> | undefined} */
110
+ #categoricalIndexer;
111
+
112
+ #categoricalIndexerExplicit = false;
113
+
94
114
  /**
95
115
  * @param {Channel} channel
96
116
  */
@@ -102,8 +122,10 @@ export default class ScaleResolution {
102
122
  /** @type {string} An optional unique identifier for the scale */
103
123
  this.name = undefined;
104
124
 
105
- this.#domainAggregator = new ScaleDomainAggregator({
106
- getMembers: () => this.#members,
125
+ this.#domainAggregator = new DomainPlanner({
126
+ getMembers: () => this.#getActiveMembers(),
127
+ getDataMembers: () =>
128
+ this.#getActiveMembers(this.#dataDomainMembers),
107
129
  getType: () => this.type,
108
130
  getLocusExtent: () => this.#getLocusExtent(),
109
131
  fromComplexInterval: this.fromComplexInterval.bind(this),
@@ -139,6 +161,29 @@ export default class ScaleResolution {
139
161
  return first.view;
140
162
  }
141
163
 
164
+ /**
165
+ * @param {Set<ScaleResolutionMember>} [members]
166
+ */
167
+ #getActiveMembers(members = this.#members) {
168
+ /** @type {Set<ScaleResolutionMember>} */
169
+ const active = new Set();
170
+ for (const member of members) {
171
+ const view = member.view;
172
+ if (!view.isConfiguredVisible()) {
173
+ continue;
174
+ }
175
+ if (
176
+ !view.isDataInitialized() &&
177
+ !member.channelDef?.scale?.domain
178
+ ) {
179
+ // Explicit domains should be honored even before data init.
180
+ continue;
181
+ }
182
+ active.add(member);
183
+ }
184
+ return active;
185
+ }
186
+
142
187
  get #viewContext() {
143
188
  return this.#firstMemberView.context;
144
189
  }
@@ -262,6 +307,10 @@ export default class ScaleResolution {
262
307
  }
263
308
 
264
309
  this.#members.add(newMember);
310
+ if (newMember.contributesToDomain) {
311
+ this.#dataDomainMembers.add(newMember);
312
+ }
313
+ this.#domainAggregator.invalidateConfiguredDomain();
265
314
  }
266
315
 
267
316
  /**
@@ -272,10 +321,62 @@ export default class ScaleResolution {
272
321
  this.#addMember(member);
273
322
  return () => {
274
323
  const removed = this.#members.delete(member);
324
+ if (removed) {
325
+ this.#dataDomainMembers.delete(member);
326
+ this.#domainAggregator.invalidateConfiguredDomain();
327
+ }
275
328
  return removed && this.#members.size === 0;
276
329
  };
277
330
  }
278
331
 
332
+ #hasRenderedMember() {
333
+ for (const member of this.#members) {
334
+ if (member.view.hasRendered()) {
335
+ return true;
336
+ }
337
+ }
338
+ return false;
339
+ }
340
+
341
+ /**
342
+ * @param {import("../data/collector.js").default} collector
343
+ * @param {Iterable<import("../types/encoder.js").ScaleAccessor>} accessors
344
+ * @returns {() => void}
345
+ */
346
+ registerCollectorSubscriptions(collector, accessors) {
347
+ /** @type {Set<string>} */
348
+ const domainKeys = new Set();
349
+
350
+ for (const accessor of accessors) {
351
+ if (accessor.channelDef.domainInert) {
352
+ continue;
353
+ }
354
+ domainKeys.add(getAccessorDomainKey(accessor, this.type));
355
+ }
356
+
357
+ if (domainKeys.size === 0) {
358
+ return () => undefined;
359
+ }
360
+
361
+ const listener = () => {
362
+ this.reconfigureDomain();
363
+ };
364
+
365
+ /** @type {(() => void)[]} */
366
+ const unregisters = [];
367
+ for (const domainKey of domainKeys) {
368
+ unregisters.push(
369
+ collector.subscribeDomainChanges(domainKey, listener)
370
+ );
371
+ }
372
+
373
+ return () => {
374
+ for (const unregister of unregisters) {
375
+ unregister();
376
+ }
377
+ };
378
+ }
379
+
279
380
  /**
280
381
  * Returns true if the domain has been defined explicitly, i.e. not extracted from the data.
281
382
  */
@@ -337,10 +438,42 @@ export default class ScaleResolution {
337
438
  extractDataDomain
338
439
  );
339
440
 
340
- if (domain && domain.length > 0) {
441
+ if (isDiscrete(props.type)) {
442
+ const isExplicit = this.#isExplicitDomain();
443
+ const indexer = this.#getCategoricalIndexer(isExplicit);
444
+ if (domain != null) {
445
+ if (
446
+ isExplicit &&
447
+ indexer.domain().length > 0 &&
448
+ !shallowArrayEquals(indexer.domain(), domain)
449
+ ) {
450
+ this.#categoricalIndexer = undefined;
451
+ return this.#getScaleProps(extractDataDomain);
452
+ }
453
+ indexer.addAll(domain);
454
+ const active = new Set(domain);
455
+ const indexedDomain = indexer
456
+ .domain()
457
+ .filter((value) => active.has(value));
458
+ props.domain =
459
+ indexedDomain.length > 0
460
+ ? /** @type {import("../spec/scale.js").ScalarDomain} */ (
461
+ indexedDomain
462
+ )
463
+ : new NominalDomain();
464
+ } else {
465
+ const indexedDomain = indexer.domain();
466
+ props.domain =
467
+ indexedDomain.length > 0
468
+ ? /** @type {import("../spec/scale.js").ScalarDomain} */ (
469
+ indexedDomain
470
+ )
471
+ : new NominalDomain();
472
+ }
473
+ // Scale props are spec-shaped; keep the indexer off the public type.
474
+ /** @type {any} */ (props).domainIndexer = indexer;
475
+ } else if (domain && domain.length > 0) {
341
476
  props.domain = domain;
342
- } else if (isDiscrete(props.type)) {
343
- props.domain = new NominalDomain();
344
477
  }
345
478
 
346
479
  if (!props.domain && props.domainMid !== undefined) {
@@ -352,6 +485,20 @@ export default class ScaleResolution {
352
485
  return props;
353
486
  }
354
487
 
488
+ /**
489
+ * @param {boolean} isExplicit
490
+ */
491
+ #getCategoricalIndexer(isExplicit) {
492
+ if (
493
+ !this.#categoricalIndexer ||
494
+ this.#categoricalIndexerExplicit !== isExplicit
495
+ ) {
496
+ this.#categoricalIndexer = createIndexer();
497
+ this.#categoricalIndexerExplicit = isExplicit;
498
+ }
499
+ return this.#categoricalIndexer;
500
+ }
501
+
355
502
  /**
356
503
  * Reconfigures the scale: updates domain and other settings.
357
504
  *
@@ -359,36 +506,108 @@ export default class ScaleResolution {
359
506
  * or when scale properties are otherwise re-resolved from the view hierarchy.
360
507
  */
361
508
  reconfigure() {
362
- const props = this.#getScaleProps(true);
363
- this.#reconfigureWith(() => this.#scaleManager.reconfigureScale(props));
509
+ this.#domainAggregator.invalidateConfiguredDomain();
510
+ const state = this.#computeScaleState(true);
511
+ if (!state) {
512
+ return;
513
+ }
514
+ this.#applyReconfigure(state, (scale, props) =>
515
+ this.#scaleManager.reconfigureScale(props)
516
+ );
517
+ this.#finalizeReconfigure(state);
364
518
  }
365
519
 
366
520
  /**
367
521
  * Reconfigures only the effective domain (configured + data-derived).
368
522
  *
369
523
  * Use this when data changes but the scale membership and properties are stable.
524
+ *
370
525
  */
371
526
  reconfigureDomain() {
372
- const props = this.#getScaleProps(true);
373
- this.#reconfigureWith(() => {
374
- configureDomain(this.#scaleManager.scale, props);
375
- });
527
+ const state = this.#computeScaleState(true, true);
528
+ if (!state) {
529
+ return;
530
+ }
531
+ const { domainConfig, targetDomain } = state;
532
+ const domainMatches =
533
+ targetDomain != null &&
534
+ shallowArrayEquals(targetDomain, state.scale.domain());
535
+
536
+ if (targetDomain != null && !domainMatches) {
537
+ this.#applyReconfigure(state, (scale) => {
538
+ scale.domain(targetDomain);
539
+ if (domainConfig.applyOrdinalUnknown) {
540
+ // Keep ordinal unknown handling close to the domain write so
541
+ // domainImplicit semantics stay aligned with the applied domain.
542
+ /** @type {any} */ (scale).unknown(
543
+ domainConfig.ordinalUnknown
544
+ );
545
+ }
546
+ });
547
+ }
548
+ this.#finalizeReconfigure(state);
376
549
  }
377
550
 
378
551
  /**
379
- * @param {() => void} apply
552
+ * @param {boolean} extractDataDomain
553
+ * @param {boolean} [includeDomainConfig]
554
+ * @returns {{
555
+ * scale: ScaleWithProps,
556
+ * props: import("../spec/scale.js").Scale,
557
+ * previousDomain: any[],
558
+ * domainWasInitialized: boolean,
559
+ * domainConfig?: ReturnType<typeof configureDomain>,
560
+ * targetDomain?: any[] | null,
561
+ * } | undefined}
380
562
  */
381
- #reconfigureWith(apply) {
563
+ #computeScaleState(extractDataDomain, includeDomainConfig = false) {
382
564
  const scale = this.#scaleManager.scale;
383
565
 
384
566
  if (!scale || scale.type == "null") {
385
567
  return;
386
568
  }
387
569
 
388
- const domainWasInitialized = this.#isDomainInitialized();
389
- const previousDomain = scale.domain();
570
+ const state = {
571
+ scale,
572
+ props: this.#getScaleProps(extractDataDomain),
573
+ previousDomain: scale.domain(),
574
+ domainWasInitialized: this.#isDomainInitialized(),
575
+ };
576
+
577
+ if (includeDomainConfig) {
578
+ const domainConfig = configureDomain(scale, state.props);
579
+ return {
580
+ ...state,
581
+ domainConfig,
582
+ targetDomain: domainConfig.domain,
583
+ };
584
+ }
585
+
586
+ return state;
587
+ }
390
588
 
391
- this.#scaleManager.withDomainNotificationsSuppressed(apply);
589
+ /**
590
+ * @param {{
591
+ * scale: ScaleWithProps,
592
+ * props: import("../spec/scale.js").Scale,
593
+ * }} inputs
594
+ * @param {(scale: ScaleWithProps, props: import("../spec/scale.js").Scale) => void} apply
595
+ */
596
+ #applyReconfigure(inputs, apply) {
597
+ this.#scaleManager.withDomainNotificationsSuppressed(() => {
598
+ apply(inputs.scale, inputs.props);
599
+ });
600
+ }
601
+
602
+ /**
603
+ * @param {{
604
+ * scale: ScaleWithProps,
605
+ * previousDomain: any[],
606
+ * domainWasInitialized: boolean,
607
+ * }} inputs
608
+ */
609
+ #finalizeReconfigure(inputs) {
610
+ const { scale, previousDomain, domainWasInitialized } = inputs;
392
611
 
393
612
  if (
394
613
  this.#domainAggregator.captureInitialDomain(
@@ -402,13 +621,18 @@ export default class ScaleResolution {
402
621
  }
403
622
 
404
623
  const newDomain = scale.domain();
405
- if (!shallowArrayEquals(newDomain, previousDomain)) {
406
- if (this.isZoomable()) {
407
- // Don't mess with zoomed views, restore the previous domain
408
- this.#scaleManager.withDomainNotificationsSuppressed(() => {
409
- scale.domain(previousDomain);
410
- });
411
- } else if (this.#interactionController.isZoomingSupported()) {
624
+ const action = this.#interactionController.getDomainChangeAction(
625
+ previousDomain,
626
+ newDomain
627
+ );
628
+
629
+ if (action === "restore") {
630
+ // Don't mess with zoomed views, restore the previous domain
631
+ this.#scaleManager.withDomainNotificationsSuppressed(() => {
632
+ scale.domain(previousDomain);
633
+ });
634
+ } else if (action === "animate") {
635
+ if (this.#hasRenderedMember()) {
412
636
  // It can be zoomed, so lets make a smooth transition.
413
637
  // Restore the previous domain and zoom smoothly to the new domain.
414
638
  this.#scaleManager.withDomainNotificationsSuppressed(() => {
@@ -416,10 +640,12 @@ export default class ScaleResolution {
416
640
  });
417
641
  this.zoomTo(newDomain, 500); // TODO: Configurable duration
418
642
  } else {
419
- // Update immediately if the previous domain was the initial domain [0, 0]
420
- // Notifications were suppressed during reconfigure; notify explicitly.
421
643
  this.#notifyListeners("domain");
422
644
  }
645
+ } else if (action === "notify") {
646
+ // Update immediately if the previous domain was the initial domain [0, 0]
647
+ // Notifications were suppressed during reconfigure; notify explicitly.
648
+ this.#notifyListeners("domain");
423
649
  }
424
650
  }
425
651
 
@@ -615,44 +841,3 @@ export default class ScaleResolution {
615
841
  return /** @type {number[]} */ (interval);
616
842
  }
617
843
  }
618
-
619
- /**
620
- * Reconfigures scale domains, starting from the given view.
621
- *
622
- * Use this for data-driven updates where only domains need refreshing.
623
- *
624
- * TODO: This should be made unnecessary. Collectors should trigger the reconfiguration
625
- * for those views that get their data from the collector.
626
- *
627
- * TODO: This may reconfigure channels that are not affected by the change.
628
- * Causes performance issues with domains that are extracted from data.
629
- *
630
- * @param {import("../view/view.js").default | import("../view/view.js").default[]} fromViews
631
- */
632
- export function reconfigureScaleDomains(fromViews) {
633
- /** @type {Set<ScaleResolution>} */
634
- const uniqueResolutions = new Set();
635
-
636
- /** @param {import("../view/view.js").default} view */
637
- function collectResolutions(view) {
638
- for (const resolution of Object.values(view.resolutions.scale)) {
639
- uniqueResolutions.add(resolution);
640
- }
641
- }
642
-
643
- for (const fromView of asArray(fromViews)) {
644
- // Descendants
645
- fromView.visit(collectResolutions);
646
-
647
- // Ancestors
648
- for (const view of fromView.getDataAncestors()) {
649
- // Skip axis views etc. They should not mess with the domains.
650
- if (!view.options.contributesToScaleDomain) {
651
- break;
652
- }
653
- collectResolutions(view);
654
- }
655
- }
656
-
657
- uniqueResolutions.forEach((resolution) => resolution.reconfigureDomain());
658
- }
@@ -1 +1 @@
1
- {"version":3,"file":"scaleResolution.test.d.ts","sourceRoot":"","sources":["../../../src/scales/scaleResolution.test.js"],"names":[],"mappings":"sBAYa,OAAO,oBAAoB,EAAE,OAAO"}
1
+ {"version":3,"file":"scaleResolution.test.d.ts","sourceRoot":"","sources":["../../../src/scales/scaleResolution.test.js"],"names":[],"mappings":"sBAWa,OAAO,oBAAoB,EAAE,OAAO"}
@@ -8,6 +8,27 @@ export function createSinglePointSelection(datum: import("../data/flowNode.js").
8
8
  * @returns {import("../types/selectionTypes.js").MultiPointSelection}
9
9
  */
10
10
  export function createMultiPointSelection(data?: import("../data/flowNode.js").Datum[]): import("../types/selectionTypes.js").MultiPointSelection;
11
+ /**
12
+ * Returns key tuples for a point selection.
13
+ *
14
+ * @param {import("../types/selectionTypes.js").SinglePointSelection | import("../types/selectionTypes.js").MultiPointSelection} selection
15
+ * @param {string[]} keyFields
16
+ * @returns {import("../spec/channel.js").Scalar[][] | undefined}
17
+ */
18
+ export function getPointSelectionKeyTuples(selection: import("../types/selectionTypes.js").SinglePointSelection | import("../types/selectionTypes.js").MultiPointSelection, keyFields: string[]): import("../spec/channel.js").Scalar[][] | undefined;
19
+ /**
20
+ * Resolves key tuples to a point selection value object.
21
+ *
22
+ * @param {"single" | "multi"} type
23
+ * @param {string[]} keyFields
24
+ * @param {import("../spec/channel.js").Scalar[][]} keyTuples
25
+ * @param {(keyFields: string[], keyTuple: import("../spec/channel.js").Scalar[]) => import("../data/flowNode.js").Datum | undefined} resolveDatum
26
+ * @returns {{ selection: import("../types/selectionTypes.js").SinglePointSelection | import("../types/selectionTypes.js").MultiPointSelection, unresolved: import("../spec/channel.js").Scalar[][] } | undefined}
27
+ */
28
+ export function resolvePointSelectionFromKeyTuples(type: "single" | "multi", keyFields: string[], keyTuples: import("../spec/channel.js").Scalar[][], resolveDatum: (keyFields: string[], keyTuple: import("../spec/channel.js").Scalar[]) => import("../data/flowNode.js").Datum | undefined): {
29
+ selection: import("../types/selectionTypes.js").SinglePointSelection | import("../types/selectionTypes.js").MultiPointSelection;
30
+ unresolved: import("../spec/channel.js").Scalar[][];
31
+ } | undefined;
11
32
  /**
12
33
  *
13
34
  * @param {import("../spec/channel.js").ChannelWithScale[]} channels
@@ -1 +1 @@
1
- {"version":3,"file":"selection.d.ts","sourceRoot":"","sources":["../../../src/selection/selection.js"],"names":[],"mappings":"AAOA;;;GAGG;AACH,kDAHW,OAAO,qBAAqB,EAAE,KAAK,GACjC,OAAO,4BAA4B,EAAE,oBAAoB,CAQrE;AAED;;;GAGG;AACH,iDAHW,OAAO,qBAAqB,EAAE,KAAK,EAAE,GACnC,OAAO,4BAA4B,EAAE,mBAAmB,CAQpE;AAED;;;;GAIG;AACH,kDAHW,OAAO,oBAAoB,EAAE,gBAAgB,EAAE,GAC7C,OAAO,4BAA4B,EAAE,iBAAiB,CASlE;AAED;;;;;;;GAOG;AACH,qDAJW,OAAO,4BAA4B,EAAE,mBAAmB,2BACxD,OAAO,CAAC,MAAM,CAAC,KAAK,GAAG,QAAQ,GAAG,QAAQ,EAAE,QAAQ,CAAC,OAAO,qBAAqB,EAAE,KAAK,CAAC,CAAC,CAAC,GACzF,OAAO,4BAA4B,EAAE,mBAAmB,CA2BpE;AAED;;;;;GAKG;AACH,oDAHW,OAAO,sBAAsB,EAAE,qBAAqB,aACpD,OAAO,4BAA4B,EAAE,SAAS,UAgExD;AAED;;;GAGG;AACH,+CAHW,OAAO,4BAA4B,EAAE,SAAS,GAC5C,SAAS,IAAI,OAAO,4BAA4B,EAAE,iBAAiB,CAI/E;AAED;;;GAGG;AACH,kDAHW,OAAO,4BAA4B,EAAE,SAAS,GAC5C,SAAS,IAAI,OAAO,4BAA4B,EAAE,oBAAoB,CAIlF;AAED;;;GAGG;AACH,iDAHW,OAAO,4BAA4B,EAAE,SAAS,GAC5C,SAAS,IAAI,OAAO,4BAA4B,EAAE,mBAAmB,CAIjF;AAED;;;GAGG;AACH,gDAHW,OAAO,4BAA4B,EAAE,SAAS,GAC5C,SAAS,IAAI,OAAO,4BAA4B,EAAE,kBAAkB,CAIhF;AAED;;;GAGG;AACH,gDAHW,OAAO,sBAAsB,EAAE,qBAAqB,GAClD,OAAO,sBAAsB,EAAE,eAAe,CA4B1D;AAED;;;GAGG;AACH,+CAHW,OAAO,sBAAsB,EAAE,eAAe,GAC5C,MAAM,IAAI,OAAO,sBAAsB,EAAE,oBAAoB,CAIzE;AAED;;;;GAIG;AACH,kDAHW,OAAO,sBAAsB,EAAE,eAAe,GAC5C,MAAM,IAAI,OAAO,sBAAsB,EAAE,uBAAuB,CAI5E;AAED;;GAEG;AACH,qDAFW,OAAO,4BAA4B,EAAE,iBAAiB,WAMhE;AAED;;;;;GAKG;AACH,kDAHW,iBAAiB,SACjB,aAAa,WAUvB;AAED;;;GAGG;AACH,yCAHW,OAAO,sBAAsB,EAAE,eAAe,CAAC,IAAI,CAAC,GAClD,OAAO,sBAAsB,EAAE,WAAW,CAsBtD;gCAvCY,OAAO,4BAA4B,EAAE,iBAAiB;4BACtD,OAAO,CAAC,MAAM,CAAC,MAAM,iBAAiB,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,CAAC"}
1
+ {"version":3,"file":"selection.d.ts","sourceRoot":"","sources":["../../../src/selection/selection.js"],"names":[],"mappings":"AAQA;;;GAGG;AACH,kDAHW,OAAO,qBAAqB,EAAE,KAAK,GACjC,OAAO,4BAA4B,EAAE,oBAAoB,CAQrE;AAED;;;GAGG;AACH,iDAHW,OAAO,qBAAqB,EAAE,KAAK,EAAE,GACnC,OAAO,4BAA4B,EAAE,mBAAmB,CAQpE;AAED;;;;;;GAMG;AACH,sDAJW,OAAO,4BAA4B,EAAE,oBAAoB,GAAG,OAAO,4BAA4B,EAAE,mBAAmB,aACpH,MAAM,EAAE,GACN,OAAO,oBAAoB,EAAE,MAAM,EAAE,EAAE,GAAG,SAAS,CA2B/D;AAED;;;;;;;;GAQG;AACH,yDANW,QAAQ,GAAG,OAAO,aAClB,MAAM,EAAE,aACR,OAAO,oBAAoB,EAAE,MAAM,EAAE,EAAE,gBACvC,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,OAAO,oBAAoB,EAAE,MAAM,EAAE,KAAK,OAAO,qBAAqB,EAAE,KAAK,GAAG,SAAS,GACvH;IAAE,SAAS,EAAE,OAAO,4BAA4B,EAAE,oBAAoB,GAAG,OAAO,4BAA4B,EAAE,mBAAmB,CAAC;IAAC,UAAU,EAAE,OAAO,oBAAoB,EAAE,MAAM,EAAE,EAAE,CAAA;CAAE,GAAG,SAAS,CAsChN;AAED;;;;GAIG;AACH,kDAHW,OAAO,oBAAoB,EAAE,gBAAgB,EAAE,GAC7C,OAAO,4BAA4B,EAAE,iBAAiB,CASlE;AAED;;;;;;;GAOG;AACH,qDAJW,OAAO,4BAA4B,EAAE,mBAAmB,2BACxD,OAAO,CAAC,MAAM,CAAC,KAAK,GAAG,QAAQ,GAAG,QAAQ,EAAE,QAAQ,CAAC,OAAO,qBAAqB,EAAE,KAAK,CAAC,CAAC,CAAC,GACzF,OAAO,4BAA4B,EAAE,mBAAmB,CA2BpE;AAED;;;;;GAKG;AACH,oDAHW,OAAO,sBAAsB,EAAE,qBAAqB,aACpD,OAAO,4BAA4B,EAAE,SAAS,UAgExD;AAED;;;GAGG;AACH,+CAHW,OAAO,4BAA4B,EAAE,SAAS,GAC5C,SAAS,IAAI,OAAO,4BAA4B,EAAE,iBAAiB,CAI/E;AAED;;;GAGG;AACH,kDAHW,OAAO,4BAA4B,EAAE,SAAS,GAC5C,SAAS,IAAI,OAAO,4BAA4B,EAAE,oBAAoB,CAIlF;AAED;;;GAGG;AACH,iDAHW,OAAO,4BAA4B,EAAE,SAAS,GAC5C,SAAS,IAAI,OAAO,4BAA4B,EAAE,mBAAmB,CAIjF;AAED;;;GAGG;AACH,gDAHW,OAAO,4BAA4B,EAAE,SAAS,GAC5C,SAAS,IAAI,OAAO,4BAA4B,EAAE,kBAAkB,CAIhF;AAED;;;GAGG;AACH,gDAHW,OAAO,sBAAsB,EAAE,qBAAqB,GAClD,OAAO,sBAAsB,EAAE,eAAe,CA4B1D;AAED;;;GAGG;AACH,+CAHW,OAAO,sBAAsB,EAAE,eAAe,GAC5C,MAAM,IAAI,OAAO,sBAAsB,EAAE,oBAAoB,CAIzE;AAED;;;;GAIG;AACH,kDAHW,OAAO,sBAAsB,EAAE,eAAe,GAC5C,MAAM,IAAI,OAAO,sBAAsB,EAAE,uBAAuB,CAI5E;AAED;;GAEG;AACH,qDAFW,OAAO,4BAA4B,EAAE,iBAAiB,WAMhE;AAED;;;;;GAKG;AACH,kDAHW,iBAAiB,SACjB,aAAa,WAUvB;AAED;;;GAGG;AACH,yCAHW,OAAO,sBAAsB,EAAE,eAAe,CAAC,IAAI,CAAC,GAClD,OAAO,sBAAsB,EAAE,WAAW,CAsBtD;gCAvCY,OAAO,4BAA4B,EAAE,iBAAiB;4BACtD,OAAO,CAAC,MAAM,CAAC,MAAM,iBAAiB,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,CAAC"}
@@ -4,6 +4,7 @@ import {
4
4
  isPrimaryPositionalChannel,
5
5
  } from "../encoder/encoder.js";
6
6
  import { validateParameterName } from "../view/paramMediator.js";
7
+ import { field } from "../utils/field.js";
7
8
 
8
9
  /**
9
10
  * @param {import("../data/flowNode.js").Datum} datum
@@ -29,6 +30,87 @@ export function createMultiPointSelection(data) {
29
30
  };
30
31
  }
31
32
 
33
+ /**
34
+ * Returns key tuples for a point selection.
35
+ *
36
+ * @param {import("../types/selectionTypes.js").SinglePointSelection | import("../types/selectionTypes.js").MultiPointSelection} selection
37
+ * @param {string[]} keyFields
38
+ * @returns {import("../spec/channel.js").Scalar[][] | undefined}
39
+ */
40
+ export function getPointSelectionKeyTuples(selection, keyFields) {
41
+ if (!keyFields || keyFields.length === 0) {
42
+ return;
43
+ }
44
+
45
+ const accessors = keyFields.map((fieldName) => field(fieldName));
46
+ const toTuple = (
47
+ /** @type {import("../data/flowNode.js").Datum} */ datum
48
+ ) => accessors.map((accessor) => accessor(datum));
49
+
50
+ if (isSinglePointSelection(selection)) {
51
+ if (!selection.datum) {
52
+ return [];
53
+ }
54
+
55
+ return [toTuple(selection.datum)];
56
+ }
57
+
58
+ if (isMultiPointSelection(selection)) {
59
+ return [...selection.data.values()].map(toTuple);
60
+ }
61
+
62
+ throw new Error(
63
+ `Expected a point selection, got: ${JSON.stringify(selection)}`
64
+ );
65
+ }
66
+
67
+ /**
68
+ * Resolves key tuples to a point selection value object.
69
+ *
70
+ * @param {"single" | "multi"} type
71
+ * @param {string[]} keyFields
72
+ * @param {import("../spec/channel.js").Scalar[][]} keyTuples
73
+ * @param {(keyFields: string[], keyTuple: import("../spec/channel.js").Scalar[]) => import("../data/flowNode.js").Datum | undefined} resolveDatum
74
+ * @returns {{ selection: import("../types/selectionTypes.js").SinglePointSelection | import("../types/selectionTypes.js").MultiPointSelection, unresolved: import("../spec/channel.js").Scalar[][] } | undefined}
75
+ */
76
+ export function resolvePointSelectionFromKeyTuples(
77
+ type,
78
+ keyFields,
79
+ keyTuples,
80
+ resolveDatum
81
+ ) {
82
+ if (!keyFields || keyFields.length === 0) {
83
+ return;
84
+ }
85
+
86
+ if (type === "single" && keyTuples.length > 1) {
87
+ throw new Error(
88
+ "Single point selections expect at most one key tuple."
89
+ );
90
+ }
91
+
92
+ /** @type {import("../data/flowNode.js").Datum[]} */
93
+ const datums = [];
94
+ /** @type {import("../spec/channel.js").Scalar[][]} */
95
+ const unresolved = [];
96
+
97
+ for (const tuple of keyTuples) {
98
+ const datum = resolveDatum(keyFields, tuple);
99
+ if (datum) {
100
+ datums.push(datum);
101
+ } else {
102
+ unresolved.push(tuple);
103
+ }
104
+ }
105
+
106
+ const selection =
107
+ type === "single"
108
+ ? createSinglePointSelection(datums[0] ?? null)
109
+ : createMultiPointSelection(datums);
110
+
111
+ return { selection, unresolved };
112
+ }
113
+
32
114
  /**
33
115
  *
34
116
  * @param {import("../spec/channel.js").ChannelWithScale[]} channels