@genome-spy/core 0.65.0 → 0.67.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 (249) hide show
  1. package/dist/bundle/browser-BRemItdO.js +138 -0
  2. package/dist/bundle/{index-CD7FLu9x.js → index-BatuyGAI.js} +23 -21
  3. package/dist/bundle/{index-C0llXMqm.js → index-ByuE8dvu.js} +140 -88
  4. package/dist/bundle/index-Cq3QFUxX.js +1781 -0
  5. package/dist/bundle/index-D28m8tSW.js +1607 -0
  6. package/dist/bundle/index-DbJ0oeYM.js +631 -0
  7. package/dist/bundle/index.es.js +15821 -14601
  8. package/dist/bundle/index.js +214 -212
  9. package/dist/bundle/{inflate-DRgHi_KK.js → inflate-GtwLkvSP.js} +222 -224
  10. package/dist/bundle/unzip-NywezaRR.js +1492 -0
  11. package/dist/schema.json +13 -3
  12. package/dist/src/config/scaleDefaults.d.ts +8 -0
  13. package/dist/src/config/scaleDefaults.d.ts.map +1 -0
  14. package/dist/src/config/scaleDefaults.js +45 -0
  15. package/dist/src/data/flowHandle.d.ts +2 -0
  16. package/dist/src/data/flowHandle.d.ts.map +1 -1
  17. package/dist/src/data/flowHandle.js +1 -0
  18. package/dist/src/data/flowInit.d.ts +12 -4
  19. package/dist/src/data/flowInit.d.ts.map +1 -1
  20. package/dist/src/data/flowInit.js +115 -16
  21. package/dist/src/data/sources/lazy/axisTickSource.js +1 -1
  22. package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts +1 -1
  23. package/dist/src/data/sources/lazy/singleAxisLazySource.d.ts.map +1 -1
  24. package/dist/src/data/sources/lazy/singleAxisLazySource.js +10 -3
  25. package/dist/src/data/sources/lazy/singleAxisWindowedSource.d.ts.map +1 -1
  26. package/dist/src/data/sources/lazy/singleAxisWindowedSource.js +5 -1
  27. package/dist/src/data/transforms/filterScoredLabels.d.ts +1 -1
  28. package/dist/src/data/transforms/filterScoredLabels.d.ts.map +1 -1
  29. package/dist/src/data/transforms/filterScoredLabels.js +1 -1
  30. package/dist/src/data/transforms/linearizeGenomicCoordinate.d.ts.map +1 -1
  31. package/dist/src/data/transforms/linearizeGenomicCoordinate.js +2 -1
  32. package/dist/src/encoder/encoder.d.ts +1 -1
  33. package/dist/src/encoder/encoder.d.ts.map +1 -1
  34. package/dist/src/encoder/encoder.js +1 -1
  35. package/dist/src/genome/scaleLocus.d.ts +39 -0
  36. package/dist/src/genome/scaleLocus.d.ts.map +1 -1
  37. package/dist/src/genome/scaleLocus.js +76 -0
  38. package/dist/src/genomeSpy/canvasExport.d.ts +19 -0
  39. package/dist/src/genomeSpy/canvasExport.d.ts.map +1 -0
  40. package/dist/src/genomeSpy/canvasExport.js +66 -0
  41. package/dist/src/genomeSpy/containerUi.d.ts +17 -0
  42. package/dist/src/genomeSpy/containerUi.d.ts.map +1 -0
  43. package/dist/src/genomeSpy/containerUi.js +78 -0
  44. package/dist/src/genomeSpy/eventListenerRegistry.d.ts +19 -0
  45. package/dist/src/genomeSpy/eventListenerRegistry.d.ts.map +1 -0
  46. package/dist/src/genomeSpy/eventListenerRegistry.js +38 -0
  47. package/dist/src/genomeSpy/inputBindingManager.d.ts +14 -0
  48. package/dist/src/genomeSpy/inputBindingManager.d.ts.map +1 -0
  49. package/dist/src/genomeSpy/inputBindingManager.js +63 -0
  50. package/dist/src/genomeSpy/interactionController.d.ts +40 -0
  51. package/dist/src/genomeSpy/interactionController.d.ts.map +1 -0
  52. package/dist/src/genomeSpy/interactionController.js +371 -0
  53. package/dist/src/genomeSpy/keyboardListenerManager.d.ts +10 -0
  54. package/dist/src/genomeSpy/keyboardListenerManager.d.ts.map +1 -0
  55. package/dist/src/genomeSpy/keyboardListenerManager.js +31 -0
  56. package/dist/src/genomeSpy/loadingIndicatorManager.d.ts +15 -0
  57. package/dist/src/genomeSpy/loadingIndicatorManager.d.ts.map +1 -0
  58. package/dist/src/genomeSpy/loadingIndicatorManager.js +92 -0
  59. package/dist/src/genomeSpy/renderCoordinator.d.ts +22 -0
  60. package/dist/src/genomeSpy/renderCoordinator.d.ts.map +1 -0
  61. package/dist/src/genomeSpy/renderCoordinator.js +118 -0
  62. package/dist/src/genomeSpy/viewContextFactory.d.ts +18 -0
  63. package/dist/src/genomeSpy/viewContextFactory.d.ts.map +1 -0
  64. package/dist/src/genomeSpy/viewContextFactory.js +79 -0
  65. package/dist/src/genomeSpy/viewDataInit.d.ts +22 -0
  66. package/dist/src/genomeSpy/viewDataInit.d.ts.map +1 -0
  67. package/dist/src/genomeSpy/viewDataInit.js +160 -0
  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/viewHierarchyConfig.d.ts +14 -0
  71. package/dist/src/genomeSpy/viewHierarchyConfig.d.ts.map +1 -0
  72. package/dist/src/genomeSpy/viewHierarchyConfig.js +24 -0
  73. package/dist/src/genomeSpy/viewHighlight.d.ts +5 -0
  74. package/dist/src/genomeSpy/viewHighlight.d.ts.map +1 -0
  75. package/dist/src/genomeSpy/viewHighlight.js +30 -0
  76. package/dist/src/genomeSpy.d.ts +17 -71
  77. package/dist/src/genomeSpy.d.ts.map +1 -1
  78. package/dist/src/genomeSpy.js +197 -741
  79. package/dist/src/gl/dataToVertices.d.ts.map +1 -1
  80. package/dist/src/gl/dataToVertices.js +16 -4
  81. package/dist/src/gl/glslScaleGenerator.d.ts +1 -1
  82. package/dist/src/gl/webGLHelper.d.ts +2 -2
  83. package/dist/src/gl/webGLHelper.d.ts.map +1 -1
  84. package/dist/src/gl/webGLHelper.js +4 -4
  85. package/dist/src/index.d.ts.map +1 -1
  86. package/dist/src/index.js +2 -12
  87. package/dist/src/marks/mark.d.ts.map +1 -1
  88. package/dist/src/marks/mark.js +4 -2
  89. package/dist/src/{view → scales}/axisResolution.d.ts +9 -16
  90. package/dist/src/scales/axisResolution.d.ts.map +1 -0
  91. package/dist/src/{view → scales}/axisResolution.js +29 -18
  92. package/dist/src/scales/axisResolution.test.d.ts.map +1 -0
  93. package/dist/src/scales/scaleDomainAggregator.d.ts +57 -0
  94. package/dist/src/scales/scaleDomainAggregator.d.ts.map +1 -0
  95. package/dist/src/scales/scaleDomainAggregator.js +167 -0
  96. package/dist/src/scales/scaleDomainAggregator.test.d.ts +2 -0
  97. package/dist/src/scales/scaleDomainAggregator.test.d.ts.map +1 -0
  98. package/dist/src/scales/scaleInstanceManager.d.ts +40 -0
  99. package/dist/src/scales/scaleInstanceManager.d.ts.map +1 -0
  100. package/dist/src/scales/scaleInstanceManager.js +317 -0
  101. package/dist/src/scales/scaleInstanceManager.test.d.ts +2 -0
  102. package/dist/src/scales/scaleInstanceManager.test.d.ts.map +1 -0
  103. package/dist/src/scales/scaleInteractionController.d.ts +73 -0
  104. package/dist/src/scales/scaleInteractionController.d.ts.map +1 -0
  105. package/dist/src/scales/scaleInteractionController.js +336 -0
  106. package/dist/src/scales/scaleInteractionController.test.d.ts +2 -0
  107. package/dist/src/scales/scaleInteractionController.test.d.ts.map +1 -0
  108. package/dist/src/scales/scalePropsResolver.d.ts +23 -0
  109. package/dist/src/scales/scalePropsResolver.d.ts.map +1 -0
  110. package/dist/src/scales/scalePropsResolver.js +74 -0
  111. package/dist/src/{view → scales}/scaleResolution.d.ts +53 -35
  112. package/dist/src/scales/scaleResolution.d.ts.map +1 -0
  113. package/dist/src/scales/scaleResolution.js +732 -0
  114. package/dist/src/scales/scaleResolution.test.d.ts.map +1 -0
  115. package/dist/src/scales/scaleResolutionConstants.d.ts +6 -0
  116. package/dist/src/scales/scaleResolutionConstants.d.ts.map +1 -0
  117. package/dist/src/scales/scaleResolutionConstants.js +5 -0
  118. package/dist/src/scales/scaleRules.d.ts +16 -0
  119. package/dist/src/scales/scaleRules.d.ts.map +1 -0
  120. package/dist/src/scales/scaleRules.js +103 -0
  121. package/dist/src/scales/scaleRules.test.d.ts +2 -0
  122. package/dist/src/scales/scaleRules.test.d.ts.map +1 -0
  123. package/dist/src/spec/channel.d.ts +13 -18
  124. package/dist/src/spec/scale.d.ts +6 -0
  125. package/dist/src/types/embedApi.d.ts +5 -0
  126. package/dist/src/types/scaleResolutionApi.d.ts +1 -1
  127. package/dist/src/utils/domainArray.d.ts.map +1 -1
  128. package/dist/src/utils/domainArray.js +3 -0
  129. package/dist/src/utils/indexer.d.ts +3 -0
  130. package/dist/src/utils/indexer.d.ts.map +1 -1
  131. package/dist/src/utils/indexer.js +3 -0
  132. package/dist/src/view/concatView.d.ts +18 -0
  133. package/dist/src/view/concatView.d.ts.map +1 -1
  134. package/dist/src/view/concatView.js +73 -0
  135. package/dist/src/view/concatView.test.d.ts +2 -0
  136. package/dist/src/view/concatView.test.d.ts.map +1 -0
  137. package/dist/src/view/containerMutationHelper.d.ts +74 -0
  138. package/dist/src/view/containerMutationHelper.d.ts.map +1 -0
  139. package/dist/src/view/containerMutationHelper.js +118 -0
  140. package/dist/src/view/containerView.d.ts +0 -7
  141. package/dist/src/view/containerView.d.ts.map +1 -1
  142. package/dist/src/view/containerView.js +0 -10
  143. package/dist/src/view/facetView.d.ts.map +1 -1
  144. package/dist/src/view/facetView.js +0 -15
  145. package/dist/src/view/flowBuilder.d.ts +5 -3
  146. package/dist/src/view/flowBuilder.d.ts.map +1 -1
  147. package/dist/src/view/flowBuilder.js +69 -6
  148. package/dist/src/view/gridView/gridChild.d.ts +11 -0
  149. package/dist/src/view/gridView/gridChild.d.ts.map +1 -1
  150. package/dist/src/view/gridView/gridChild.js +32 -6
  151. package/dist/src/view/gridView/gridView.d.ts +39 -1
  152. package/dist/src/view/gridView/gridView.d.ts.map +1 -1
  153. package/dist/src/view/gridView/gridView.js +106 -48
  154. package/dist/src/view/gridView/gridView.test.d.ts +2 -0
  155. package/dist/src/view/gridView/gridView.test.d.ts.map +1 -0
  156. package/dist/src/view/gridView/scrollbar.d.ts +39 -8
  157. package/dist/src/view/gridView/scrollbar.d.ts.map +1 -1
  158. package/dist/src/view/gridView/scrollbar.js +184 -69
  159. package/dist/src/view/layerView.d.ts +14 -0
  160. package/dist/src/view/layerView.d.ts.map +1 -1
  161. package/dist/src/view/layerView.js +66 -0
  162. package/dist/src/view/layerView.test.d.ts +2 -0
  163. package/dist/src/view/layerView.test.d.ts.map +1 -0
  164. package/dist/src/view/testUtils.d.ts.map +1 -1
  165. package/dist/src/view/testUtils.js +7 -1
  166. package/dist/src/view/unitView.d.ts.map +1 -1
  167. package/dist/src/view/unitView.js +41 -36
  168. package/dist/src/view/view.d.ts +18 -6
  169. package/dist/src/view/view.d.ts.map +1 -1
  170. package/dist/src/view/view.js +30 -4
  171. package/package.json +2 -2
  172. package/dist/bundle/browser-txUcLy2H.js +0 -123
  173. package/dist/bundle/index-BQpbYrv4.js +0 -1712
  174. package/dist/bundle/index-BhtHKLUo.js +0 -73
  175. package/dist/bundle/index-CCe8rnZz.js +0 -716
  176. package/dist/bundle/index-DhcU-Gk-.js +0 -1487
  177. package/dist/src/data/collector.test.js +0 -138
  178. package/dist/src/data/dataFlow.test.js +0 -38
  179. package/dist/src/data/flow.test.js +0 -81
  180. package/dist/src/data/flowInit.test.js +0 -413
  181. package/dist/src/data/flowNode.test.js +0 -50
  182. package/dist/src/data/flowOptimizer.test.js +0 -209
  183. package/dist/src/data/formats/fasta.test.js +0 -27
  184. package/dist/src/data/sources/inlineSource.test.js +0 -63
  185. package/dist/src/data/sources/sequenceSource.test.js +0 -81
  186. package/dist/src/data/transforms/aggregate.test.js +0 -134
  187. package/dist/src/data/transforms/clone.test.js +0 -11
  188. package/dist/src/data/transforms/coverage.test.js +0 -238
  189. package/dist/src/data/transforms/filter.test.js +0 -20
  190. package/dist/src/data/transforms/flatten.test.js +0 -96
  191. package/dist/src/data/transforms/flattenDelimited.test.js +0 -90
  192. package/dist/src/data/transforms/flattenSequence.test.js +0 -34
  193. package/dist/src/data/transforms/formula.test.js +0 -25
  194. package/dist/src/data/transforms/identifier.test.js +0 -92
  195. package/dist/src/data/transforms/pileup.test.js +0 -70
  196. package/dist/src/data/transforms/project.test.js +0 -32
  197. package/dist/src/data/transforms/regexExtract.test.js +0 -70
  198. package/dist/src/data/transforms/regexFold.test.js +0 -201
  199. package/dist/src/data/transforms/sample.test.js +0 -38
  200. package/dist/src/data/transforms/stack.test.js +0 -91
  201. package/dist/src/encoder/accessor.test.js +0 -162
  202. package/dist/src/encoder/encoder.test.js +0 -105
  203. package/dist/src/genome/genome.test.js +0 -268
  204. package/dist/src/genome/genomes.test.js +0 -8
  205. package/dist/src/genome/scaleIndex.test.js +0 -78
  206. package/dist/src/genome/scaleLocus.test.js +0 -4
  207. package/dist/src/scale/scale.test.js +0 -326
  208. package/dist/src/scale/ticks.test.js +0 -46
  209. package/dist/src/selection/selection.test.js +0 -14
  210. package/dist/src/utils/addBaseUrl.test.js +0 -30
  211. package/dist/src/utils/binnedIndex.test.js +0 -201
  212. package/dist/src/utils/cloner.test.js +0 -35
  213. package/dist/src/utils/coalesce.test.js +0 -16
  214. package/dist/src/utils/concatIterables.test.js +0 -8
  215. package/dist/src/utils/domainArray.test.js +0 -130
  216. package/dist/src/utils/indexer.test.js +0 -49
  217. package/dist/src/utils/interactionEvent.test.js +0 -35
  218. package/dist/src/utils/iterateNestedMaps.test.js +0 -33
  219. package/dist/src/utils/kWayMerge.test.js +0 -30
  220. package/dist/src/utils/mergeObjects.test.js +0 -42
  221. package/dist/src/utils/numberExtractor.test.js +0 -6
  222. package/dist/src/utils/propertyCacher.test.js +0 -89
  223. package/dist/src/utils/propertyCoalescer.test.js +0 -25
  224. package/dist/src/utils/radixSort.test.js +0 -51
  225. package/dist/src/utils/reservationMap.test.js +0 -20
  226. package/dist/src/utils/ringBuffer.test.js +0 -39
  227. package/dist/src/utils/topK.test.js +0 -54
  228. package/dist/src/utils/trees.test.js +0 -135
  229. package/dist/src/utils/url.test.js +0 -28
  230. package/dist/src/utils/variableTools.test.js +0 -13
  231. package/dist/src/view/axisResolution.d.ts.map +0 -1
  232. package/dist/src/view/axisResolution.test.d.ts.map +0 -1
  233. package/dist/src/view/axisResolution.test.js +0 -206
  234. package/dist/src/view/flowBuilder.test.js +0 -125
  235. package/dist/src/view/gridView/selectionRect.test.js +0 -87
  236. package/dist/src/view/layout/flexLayout.test.js +0 -323
  237. package/dist/src/view/layout/grid.test.js +0 -71
  238. package/dist/src/view/layout/rectangle.test.js +0 -192
  239. package/dist/src/view/paramMediator.test.js +0 -282
  240. package/dist/src/view/scaleResolution.d.ts.map +0 -1
  241. package/dist/src/view/scaleResolution.js +0 -1059
  242. package/dist/src/view/scaleResolution.test.d.ts.map +0 -1
  243. package/dist/src/view/scaleResolution.test.js +0 -645
  244. package/dist/src/view/view.test.js +0 -245
  245. package/dist/src/view/viewDispose.test.js +0 -110
  246. package/dist/src/view/viewFactory.test.js +0 -25
  247. package/dist/src/view/viewUtils.test.js +0 -87
  248. /package/dist/src/{view → scales}/axisResolution.test.d.ts +0 -0
  249. /package/dist/src/{view → scales}/scaleResolution.test.d.ts +0 -0
@@ -0,0 +1,732 @@
1
+ import scaleLocus, {
2
+ fromComplexInterval as locusFromComplexInterval,
3
+ fromComplexValue,
4
+ getGenomeExtent,
5
+ toComplexInterval,
6
+ toComplexValue,
7
+ } from "../genome/scaleLocus.js";
8
+ import scaleIndex from "../genome/scaleIndex.js";
9
+ import scaleNull from "../utils/scaleNull.js";
10
+
11
+ import { scale as vegaScale, isDiscrete, isContinuous } from "vega-scale";
12
+ import { configureDomain } from "../scale/scale.js";
13
+
14
+ import ScaleInstanceManager from "./scaleInstanceManager.js";
15
+ import { resolveScalePropsBase } from "./scalePropsResolver.js";
16
+ import ScaleDomainAggregator from "./scaleDomainAggregator.js";
17
+ import ScaleInteractionController from "./scaleInteractionController.js";
18
+ import {
19
+ INDEX,
20
+ LOCUS,
21
+ NOMINAL,
22
+ ORDINAL,
23
+ QUANTITATIVE,
24
+ } from "./scaleResolutionConstants.js";
25
+
26
+ import { isSecondaryChannel } from "../encoder/encoder.js";
27
+ import { NominalDomain } from "../utils/domainArray.js";
28
+ import { asArray, shallowArrayEquals } from "../utils/arrayUtils.js";
29
+ import { VISIT_SKIP } from "../view/view.js";
30
+ import createIndexer from "../utils/indexer.js";
31
+
32
+ // Register scaleLocus to Vega-Scale.
33
+ // Loci are discrete but the scale's domain can be adjusted in a continuous manner.
34
+ vegaScale("index", scaleIndex, ["continuous"]);
35
+ vegaScale("locus", scaleLocus, ["continuous"]);
36
+ vegaScale("null", scaleNull, []);
37
+
38
+ export { INDEX, LOCUS, NOMINAL, ORDINAL, QUANTITATIVE };
39
+
40
+ /**
41
+ * @template {ChannelWithScale}[T=ChannelWithScale]
42
+ *
43
+ * @typedef {object} ScaleResolutionMember
44
+ * @prop {import("../view/unitView.js").default} view TODO: Get rid of the view reference
45
+ * @prop {T} channel
46
+ * @prop {import("../spec/channel.js").ChannelDefWithScale} channelDef
47
+ * @prop {(channel: ChannelWithScale, type: import("../spec/channel.js").Type) => DomainArray} dataDomainSource
48
+ */
49
+ /**
50
+ * Resolves a shared scale for a channel by merging scale properties and domains
51
+ * across participating views, then coordinating range updates and zoom/pan
52
+ * interactions. It is the central wiring point for scale-related state and
53
+ * notifications, while delegating domain aggregation, scale instance setup, and
54
+ * interaction logic to focused helpers.
55
+ *
56
+ * @implements {ScaleResolutionApi}
57
+ */
58
+ export default class ScaleResolution {
59
+ /**
60
+ * @typedef {import("../types/scaleResolutionApi.js").ScaleResolutionApi} ScaleResolutionApi
61
+ * @typedef {import("../types/scaleResolutionApi.js").ScaleResolutionEventType} ScaleResolutionEventType
62
+ * @typedef {import("../spec/channel.js").Channel} Channel
63
+ * @typedef {import("../spec/channel.js").ChannelWithScale} ChannelWithScale
64
+ * @typedef {import("../spec/scale.js").NumericDomain} NumericDomain
65
+ * @typedef {import("../spec/scale.js").ScalarDomain} ScalarDomain
66
+ * @typedef {import("../spec/scale.js").ComplexDomain} ComplexDomain
67
+ * @typedef {import("../view/unitView.js").default} UnitView
68
+ * @typedef {import("../types/encoder.js").VegaScale} VegaScale
69
+ * @typedef {import("../utils/domainArray.js").DomainArray} DomainArray
70
+ * @typedef {import("../genome/genome.js").ChromosomalLocus} ChromosomalLocus
71
+ * @typedef {import("../types/scaleResolutionApi.js").ScaleResolutionListener} ScaleResolutionListener
72
+ *
73
+ * @typedef {VegaScale & { props: import("../spec/scale.js").Scale }} ScaleWithProps
74
+ */
75
+
76
+ /** @type {Set<ScaleResolutionMember>} The involved views */
77
+ #members = new Set();
78
+
79
+ /**
80
+ * @type {Record<ScaleResolutionEventType, Set<ScaleResolutionListener>>}
81
+ */
82
+ #listeners = {
83
+ domain: new Set(),
84
+ range: new Set(),
85
+ };
86
+
87
+ /** @type {ScaleInstanceManager} */
88
+ #scaleManager;
89
+
90
+ /** @type {ScaleDomainAggregator} */
91
+ #domainAggregator;
92
+
93
+ /** @type {ScaleInteractionController} */
94
+ #interactionController;
95
+
96
+ /** @type {ReturnType<typeof createIndexer> | undefined} */
97
+ #categoricalIndexer;
98
+
99
+ #categoricalIndexerExplicit = false;
100
+
101
+ /**
102
+ * @param {Channel} channel
103
+ */
104
+ constructor(channel) {
105
+ this.channel = channel;
106
+ /** @type {import("../spec/channel.js").Type} Data type (quantitative, nominal, etc...) */
107
+ this.type = null;
108
+
109
+ /** @type {string} An optional unique identifier for the scale */
110
+ this.name = undefined;
111
+
112
+ this.#domainAggregator = new ScaleDomainAggregator({
113
+ getMembers: () => this.#getActiveMembers(),
114
+ getType: () => this.type,
115
+ getLocusExtent: () => this.#getLocusExtent(),
116
+ fromComplexInterval: this.fromComplexInterval.bind(this),
117
+ });
118
+
119
+ this.#scaleManager = new ScaleInstanceManager({
120
+ getParamMediator: () => this.#firstMemberView.paramMediator,
121
+ onRangeChange: () => this.#notifyListeners("range"),
122
+ onDomainChange: () => this.#notifyListeners("domain"),
123
+ getGenomeStore: () => this.#viewContext.genomeStore,
124
+ });
125
+
126
+ this.#interactionController = new ScaleInteractionController({
127
+ getScale: () => this.getScale(),
128
+ getAnimator: () => this.#viewContext.animator,
129
+ getInitialDomainSnapshot: () =>
130
+ this.#domainAggregator.initialDomainSnapshot,
131
+ getResetDomain: () =>
132
+ this.#domainAggregator.getConfiguredOrDefaultDomain(),
133
+ fromComplexInterval: this.fromComplexInterval.bind(this),
134
+ getGenomeExtent: () => this.#getLocusExtent(),
135
+ });
136
+ }
137
+
138
+ /**
139
+ * @returns {import("../view/view.js").default}
140
+ */
141
+ get #firstMemberView() {
142
+ const first = this.#members.values().next().value;
143
+ if (!first) {
144
+ throw new Error("ScaleResolution has no members!");
145
+ }
146
+ return first.view;
147
+ }
148
+
149
+ #getActiveMembers() {
150
+ /** @type {Set<ScaleResolutionMember>} */
151
+ const active = new Set();
152
+ for (const member of this.#members) {
153
+ const view = member.view;
154
+ if (!view.isConfiguredVisible()) {
155
+ continue;
156
+ }
157
+ if (
158
+ !view.isDataInitialized() &&
159
+ !member.channelDef?.scale?.domain
160
+ ) {
161
+ // Explicit domains should be honored even before data init.
162
+ continue;
163
+ }
164
+ active.add(member);
165
+ }
166
+ return active;
167
+ }
168
+
169
+ get #viewContext() {
170
+ return this.#firstMemberView.context;
171
+ }
172
+
173
+ get zoomExtent() {
174
+ return (
175
+ (this.#scaleManager.scale &&
176
+ isContinuous(this.#scaleManager.scale.type) &&
177
+ this.#interactionController.getZoomExtent()) ?? [
178
+ -Infinity,
179
+ Infinity,
180
+ ]
181
+ );
182
+ }
183
+
184
+ /**
185
+ * @returns {number[]}
186
+ */
187
+ #getLocusExtent() {
188
+ return getGenomeExtent(this.#getGenomeSource());
189
+ }
190
+
191
+ /**
192
+ * @returns {import("../genome/scaleLocus.js").GenomeSource}
193
+ */
194
+ #getGenomeSource() {
195
+ if (this.type !== LOCUS) {
196
+ return undefined;
197
+ }
198
+ return /** @type {import("../genome/scaleLocus.js").GenomeSource} */ (
199
+ this.#scaleManager.scale ?? this.#scaleManager.getLocusGenome()
200
+ );
201
+ }
202
+
203
+ /**
204
+ * Adds a listener that is called when the scale domain is changed,
205
+ * e.g., zoomed. The call is synchronous and happens before the views
206
+ * are rendered.
207
+ *
208
+ * @param {ScaleResolutionEventType} type
209
+ * @param {ScaleResolutionListener} listener function
210
+ */
211
+ addEventListener(type, listener) {
212
+ this.#listeners[type].add(listener);
213
+ }
214
+
215
+ /**
216
+ * @param {ScaleResolutionEventType} type
217
+ * @param {ScaleResolutionListener} listener function
218
+ */
219
+ removeEventListener(type, listener) {
220
+ this.#listeners[type].delete(listener);
221
+ }
222
+
223
+ /**
224
+ * @param {ScaleResolutionEventType} type
225
+ */
226
+ #notifyListeners(type) {
227
+ for (const listener of this.#listeners[type].values()) {
228
+ listener({
229
+ type,
230
+ scaleResolution: this,
231
+ });
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Add a view to this resolution.
237
+ * N.B. This is expected to be called in depth-first order
238
+ *
239
+ * @param {ScaleResolutionMember} newMember
240
+ */
241
+ #addMember(newMember) {
242
+ const { channel, channelDef } = newMember;
243
+
244
+ // A convenience hack for cases where the new member should adapt
245
+ // the scale type to the existing one. For example: SelectionRect
246
+ // TODO: Add test
247
+ const adapt = channelDef.type == null && this.type;
248
+
249
+ if (
250
+ // @ts-expect-error "sample" is not really a channel with scale
251
+ channel != "sample" &&
252
+ !channelDef.type &&
253
+ !isSecondaryChannel(channel) &&
254
+ !adapt
255
+ ) {
256
+ throw new Error(
257
+ `The "type" property must be defined in channel definition: "${channel}": ${JSON.stringify(
258
+ channelDef
259
+ )}. Must be one of: "quantitative", "ordinal", "nominal", "locus", "index"`
260
+ );
261
+ }
262
+
263
+ // A hack for sample channel, which really doesn't have a scale but the
264
+ // domain is needed when samples are not specified explicitly.
265
+ // @ts-expect-error "sample" is not really a channel with scale
266
+ const type = channel == "sample" ? "nominal" : channelDef.type;
267
+ const name = channelDef?.scale?.name;
268
+
269
+ if (name) {
270
+ if (this.name !== undefined && name != this.name) {
271
+ throw new Error(
272
+ `Shared scales have conflicting names: "${name}" vs. "${this.name}"!`
273
+ );
274
+ }
275
+ this.name = name;
276
+ }
277
+
278
+ if (!adapt) {
279
+ if (!this.type) {
280
+ this.type = type;
281
+ } else if (type !== this.type && !isSecondaryChannel(channel)) {
282
+ // TODO: Include a reference to the layer
283
+ throw new Error(
284
+ `Can not use shared scale for different data types: ${this.type} vs. ${type}. Use "resolve: independent" for channel ${this.channel}`
285
+ );
286
+ // Actually, point scale could be changed into band scale
287
+ // TODO: Use the same merging logic as in: https://github.com/vega/vega-lite/blob/master/src/scale.ts
288
+ }
289
+ }
290
+
291
+ this.#members.add(newMember);
292
+ }
293
+
294
+ /**
295
+ * @param {ScaleResolutionMember} member
296
+ * @returns {() => boolean}
297
+ */
298
+ registerMember(member) {
299
+ this.#addMember(member);
300
+ return () => {
301
+ const removed = this.#members.delete(member);
302
+ return removed && this.#members.size === 0;
303
+ };
304
+ }
305
+
306
+ /**
307
+ * Returns true if the domain has been defined explicitly, i.e. not extracted from the data.
308
+ */
309
+ #isExplicitDomain() {
310
+ return this.#domainAggregator.hasConfiguredDomain();
311
+ }
312
+
313
+ #isDomainInitialized() {
314
+ const s = this.#scaleManager.scale;
315
+ if (!s) {
316
+ return false;
317
+ }
318
+
319
+ const domain = s.domain();
320
+
321
+ // We could alternatively have a flag that is set when the domain is initialized.
322
+ if (isContinuous(s.type)) {
323
+ return (
324
+ domain.length > 2 ||
325
+ (domain.length == 2 && (domain[0] !== 0 || domain[1] !== 0))
326
+ );
327
+ } else {
328
+ return domain.length > 0;
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Collects and merges scale properties from the participating views.
334
+ * Does not include inferred default values such as schemes etc.
335
+ *
336
+ * @returns {import("../spec/scale.js").Scale}
337
+ */
338
+ #getMergedScaleProps() {
339
+ return resolveScalePropsBase({
340
+ channel: this.channel,
341
+ dataType: this.type,
342
+ members: this.#members,
343
+ isExplicitDomain: this.#isExplicitDomain(),
344
+ });
345
+ }
346
+
347
+ /**
348
+ * Returns the merged scale properties supplemented with inferred properties
349
+ * and domain.
350
+ *
351
+ * @param {boolean} [extractDataDomain]
352
+ * @returns {import("../spec/scale.js").Scale}
353
+ */
354
+ #getScaleProps(extractDataDomain = false) {
355
+ const props = this.#getMergedScaleProps();
356
+ if (props === null || props.type == "null") {
357
+ // No scale (pass-thru)
358
+ // TODO: Check that the channel is compatible
359
+ return { type: "null" };
360
+ }
361
+
362
+ const domain =
363
+ this.#domainAggregator.getConfiguredOrDefaultDomain(
364
+ extractDataDomain
365
+ );
366
+
367
+ if (isDiscrete(props.type)) {
368
+ const isExplicit = this.#isExplicitDomain();
369
+ const indexer = this.#getCategoricalIndexer(isExplicit);
370
+ if (domain != null) {
371
+ if (
372
+ isExplicit &&
373
+ indexer.domain().length > 0 &&
374
+ !shallowArrayEquals(indexer.domain(), domain)
375
+ ) {
376
+ this.#categoricalIndexer = undefined;
377
+ return this.#getScaleProps(extractDataDomain);
378
+ }
379
+ indexer.addAll(domain);
380
+ const active = new Set(domain);
381
+ const indexedDomain = indexer
382
+ .domain()
383
+ .filter((value) => active.has(value));
384
+ props.domain =
385
+ indexedDomain.length > 0
386
+ ? /** @type {import("../spec/scale.js").ScalarDomain} */ (
387
+ indexedDomain
388
+ )
389
+ : new NominalDomain();
390
+ } else {
391
+ const indexedDomain = indexer.domain();
392
+ props.domain =
393
+ indexedDomain.length > 0
394
+ ? /** @type {import("../spec/scale.js").ScalarDomain} */ (
395
+ indexedDomain
396
+ )
397
+ : new NominalDomain();
398
+ }
399
+ // Scale props are spec-shaped; keep the indexer off the public type.
400
+ /** @type {any} */ (props).domainIndexer = indexer;
401
+ } else if (domain && domain.length > 0) {
402
+ props.domain = domain;
403
+ }
404
+
405
+ if (!props.domain && props.domainMid !== undefined) {
406
+ // Initialize with a bogus domain so that scale.js can inject the domainMid.
407
+ // The number of domain elements must be know before the glsl scale is generated.
408
+ props.domain = [props.domainMin ?? 0, props.domainMax ?? 1];
409
+ }
410
+
411
+ return props;
412
+ }
413
+
414
+ /**
415
+ * @param {boolean} isExplicit
416
+ */
417
+ #getCategoricalIndexer(isExplicit) {
418
+ if (
419
+ !this.#categoricalIndexer ||
420
+ this.#categoricalIndexerExplicit !== isExplicit
421
+ ) {
422
+ this.#categoricalIndexer = createIndexer();
423
+ this.#categoricalIndexerExplicit = isExplicit;
424
+ }
425
+ return this.#categoricalIndexer;
426
+ }
427
+
428
+ /**
429
+ * Reconfigures the scale: updates domain and other settings.
430
+ *
431
+ * Use this when the set of participating members changes (views added or removed),
432
+ * or when scale properties are otherwise re-resolved from the view hierarchy.
433
+ */
434
+ reconfigure() {
435
+ const props = this.#getScaleProps(true);
436
+ this.#reconfigureWith(() => this.#scaleManager.reconfigureScale(props));
437
+ }
438
+
439
+ /**
440
+ * Reconfigures only the effective domain (configured + data-derived).
441
+ *
442
+ * Use this when data changes but the scale membership and properties are stable.
443
+ */
444
+ reconfigureDomain() {
445
+ const props = this.#getScaleProps(true);
446
+ this.#reconfigureWith(() => {
447
+ configureDomain(this.#scaleManager.scale, props);
448
+ });
449
+ }
450
+
451
+ /**
452
+ * @param {() => void} apply
453
+ */
454
+ #reconfigureWith(apply) {
455
+ const scale = this.#scaleManager.scale;
456
+
457
+ if (!scale || scale.type == "null") {
458
+ return;
459
+ }
460
+
461
+ const domainWasInitialized = this.#isDomainInitialized();
462
+ const previousDomain = scale.domain();
463
+
464
+ this.#scaleManager.withDomainNotificationsSuppressed(apply);
465
+
466
+ if (
467
+ this.#domainAggregator.captureInitialDomain(
468
+ scale,
469
+ domainWasInitialized
470
+ )
471
+ ) {
472
+ // Domain changes were suppressed during reconfigure; notify explicitly.
473
+ this.#notifyListeners("domain");
474
+ return;
475
+ }
476
+
477
+ const newDomain = scale.domain();
478
+ if (!shallowArrayEquals(newDomain, previousDomain)) {
479
+ if (this.isZoomable()) {
480
+ // Don't mess with zoomed views, restore the previous domain
481
+ this.#scaleManager.withDomainNotificationsSuppressed(() => {
482
+ scale.domain(previousDomain);
483
+ });
484
+ } else if (this.#interactionController.isZoomingSupported()) {
485
+ // It can be zoomed, so lets make a smooth transition.
486
+ // Restore the previous domain and zoom smoothly to the new domain.
487
+ this.#scaleManager.withDomainNotificationsSuppressed(() => {
488
+ scale.domain(previousDomain);
489
+ });
490
+ this.zoomTo(newDomain, 500); // TODO: Configurable duration
491
+ } else {
492
+ // Update immediately if the previous domain was the initial domain [0, 0]
493
+ // Notifications were suppressed during reconfigure; notify explicitly.
494
+ this.#notifyListeners("domain");
495
+ }
496
+ }
497
+ }
498
+
499
+ /**
500
+ * @returns {ScaleWithProps}
501
+ */
502
+ get scale() {
503
+ if (this.#scaleManager.scale) {
504
+ return this.#scaleManager.scale;
505
+ }
506
+ throw new Error(
507
+ "ScaleResolution.scale accessed before initialization. Call initializeScale()."
508
+ );
509
+ }
510
+
511
+ /**
512
+ * Returns the scale instance, creating it if needed.
513
+ *
514
+ * Use this from call sites that may run before explicit initialization.
515
+ *
516
+ * @returns {ScaleWithProps}
517
+ */
518
+ getScale() {
519
+ return this.#scaleManager.scale ?? this.initializeScale();
520
+ }
521
+
522
+ /**
523
+ * Initializes the scale instance once resolution has stabilized.
524
+ *
525
+ * @returns {ScaleWithProps}
526
+ */
527
+ initializeScale() {
528
+ if (this.#scaleManager.scale) {
529
+ return this.#scaleManager.scale;
530
+ }
531
+
532
+ const props = this.#getScaleProps();
533
+ const scale = this.#scaleManager.createScale(props);
534
+
535
+ return scale;
536
+ }
537
+
538
+ getDomain() {
539
+ return this.getScale().domain();
540
+ }
541
+
542
+ /**
543
+ * Extracts and unions the data domains of all participating views.
544
+ *
545
+ * @return { DomainArray }
546
+ */
547
+ getDataDomain() {
548
+ return this.#domainAggregator.getDataDomain();
549
+ }
550
+
551
+ /**
552
+ * @returns {NumericDomain | ComplexDomain}
553
+ */
554
+ getComplexDomain() {
555
+ return /** @type {NumericDomain | ComplexDomain} */ (
556
+ toComplexInterval(this.#getGenomeSource(), this.getDomain())
557
+ );
558
+ }
559
+
560
+ /**
561
+ * Return true if the scale is zoomable and the current domain differs from the initial domain.
562
+ *
563
+ * @returns true if zoomed
564
+ */
565
+ isZoomed() {
566
+ return this.#interactionController.isZoomed();
567
+ }
568
+
569
+ /**
570
+ * Returns true if zooming is supported and allowed in view spec.
571
+ */
572
+ isZoomable() {
573
+ // Check explicit configuration
574
+ return this.#interactionController.isZoomable();
575
+ }
576
+
577
+ /**
578
+ * Pans (translates) and zooms using a specified scale factor.
579
+ *
580
+ * @param {number} scaleFactor
581
+ * @param {number} scaleAnchor
582
+ * @param {number} pan
583
+ * @returns {boolean} true if the scale was zoomed
584
+ */
585
+ zoom(scaleFactor, scaleAnchor, pan) {
586
+ return this.#interactionController.zoom(scaleFactor, scaleAnchor, pan);
587
+ }
588
+
589
+ /**
590
+ * Immediately zooms to the given interval.
591
+ *
592
+ * @param {NumericDomain | ComplexDomain} domain
593
+ * @param {boolean | number} [duration] an approximate duration for transition.
594
+ * Zero duration zooms immediately. Boolean `true` indicates a default duration.
595
+ */
596
+ async zoomTo(domain, duration = false) {
597
+ return this.#interactionController.zoomTo(domain, duration);
598
+ }
599
+
600
+ /**
601
+ * Resets the current domain to the initial one
602
+ *
603
+ * @returns true if the domain was changed
604
+ */
605
+ resetZoom() {
606
+ return this.#interactionController.resetZoom();
607
+ }
608
+
609
+ /**
610
+ * Returns the zoom level with respect to the reference domain span (the original domain).
611
+ *
612
+ * In principle, this is highly specific to positional channels. However, zooming can
613
+ * be generalized to other quantitative channels such as color, opacity, size, etc.
614
+ */
615
+ getZoomLevel() {
616
+ return this.#interactionController.getZoomLevel();
617
+ }
618
+
619
+ /**
620
+ * Returns the length of the axis in pixels. Chooses the smallest of the views.
621
+ * They should all be the same, but some exotic configuration might break that assumption.
622
+ *
623
+ * This method is needed because positional channels have unit ranges and the
624
+ * length of the axis is not directly available from the scale. Ideally, ranges would
625
+ * be configured as pixels, but that is yet to be materialized.
626
+ */
627
+ getAxisLength() {
628
+ if (this.channel !== "x" && this.channel !== "y") {
629
+ throw new Error(
630
+ "Axis length is only defined for x and y channels!"
631
+ );
632
+ }
633
+
634
+ // Here's a problem: if the view has been hidden, it may have stale coords.
635
+ // TODO: They should be cleared when the layout is invalidated.
636
+ // Alternatively, scale ranges could be set in pixels.
637
+ const lengths = Array.from(this.#members)
638
+ .map(
639
+ (m) =>
640
+ m.view.coords?.[this.channel === "x" ? "width" : "height"]
641
+ )
642
+ .filter((len) => len > 0);
643
+
644
+ return lengths.length
645
+ ? lengths.reduce((a, b) => Math.min(a, b), 10000)
646
+ : 0;
647
+ }
648
+
649
+ /**
650
+ * Inverts a value in range to a value on domain. Returns an object in
651
+ * case of locus scale.
652
+ *
653
+ * @param {number} value
654
+ */
655
+ invertToComplex(value) {
656
+ const scale = this.getScale();
657
+ if ("invert" in scale) {
658
+ const inverted = /** @type {number} */ (scale.invert(value));
659
+ return this.toComplex(inverted);
660
+ } else {
661
+ throw new Error("The scale does not support inverting!");
662
+ }
663
+ }
664
+
665
+ /**
666
+ * @param {number} value
667
+ */
668
+ toComplex(value) {
669
+ return toComplexValue(this.#getGenomeSource(), value);
670
+ }
671
+
672
+ /**
673
+ * @param {number | ChromosomalLocus} complex
674
+ * @returns {number}
675
+ */
676
+ fromComplex(complex) {
677
+ return fromComplexValue(this.#getGenomeSource(), complex);
678
+ }
679
+
680
+ /**
681
+ * @param {ScalarDomain | ComplexDomain} interval
682
+ * @returns {number[]}
683
+ */
684
+ fromComplexInterval(interval) {
685
+ if (this.type == LOCUS) {
686
+ return locusFromComplexInterval(this.#getGenomeSource(), interval);
687
+ }
688
+ return /** @type {number[]} */ (interval);
689
+ }
690
+ }
691
+
692
+ /**
693
+ * Reconfigures scale domains for resolutions used by the given view(s).
694
+ *
695
+ * Use this for data-driven updates where only domains need refreshing.
696
+ *
697
+ * TODO: This should be made unnecessary. Collectors should trigger the reconfiguration
698
+ * for those views that get their data from the collector.
699
+ *
700
+ * TODO: This may reconfigure channels that are not affected by the change.
701
+ * Causes performance issues with domains that are extracted from data.
702
+ *
703
+ * @param {import("../view/view.js").default | import("../view/view.js").default[]} fromViews
704
+ * @param {(view: import("../view/view.js").default) => boolean} [viewFilter]
705
+ */
706
+ export function reconfigureScaleDomains(fromViews, viewFilter) {
707
+ /** @type {Set<ScaleResolution>} */
708
+ const uniqueResolutions = new Set();
709
+
710
+ /** @param {import("../view/view.js").default} view */
711
+ function collectResolutions(view) {
712
+ for (const resolution of Object.values(view.resolutions.scale)) {
713
+ uniqueResolutions.add(resolution);
714
+ }
715
+ }
716
+
717
+ /** @type {import("../view/view.js").VisitorCallback} */
718
+ function collectVisibleResolutions(view) {
719
+ if (viewFilter && !viewFilter(view)) {
720
+ return VISIT_SKIP;
721
+ }
722
+ if (view.options.contributesToScaleDomain) {
723
+ collectResolutions(view);
724
+ }
725
+ }
726
+
727
+ for (const fromView of asArray(fromViews)) {
728
+ fromView.visit(collectVisibleResolutions);
729
+ }
730
+
731
+ uniqueResolutions.forEach((resolution) => resolution.reconfigureDomain());
732
+ }