@genome-spy/core 0.14.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 (226) hide show
  1. package/dist/index.js +224 -0
  2. package/dist/style.css +1 -0
  3. package/package.json +54 -0
  4. package/src/data/collector.js +178 -0
  5. package/src/data/collector.test.js +82 -0
  6. package/src/data/dataFlow.js +109 -0
  7. package/src/data/dataFlow.test.js +3 -0
  8. package/src/data/facetNode.js +17 -0
  9. package/src/data/flow.test.js +71 -0
  10. package/src/data/flowBatch.d.ts +40 -0
  11. package/src/data/flowNode.js +283 -0
  12. package/src/data/flowNode.test.js +49 -0
  13. package/src/data/flowOptimizer.js +117 -0
  14. package/src/data/flowOptimizer.test.js +192 -0
  15. package/src/data/flowTestUtils.js +63 -0
  16. package/src/data/formats/fasta.js +32 -0
  17. package/src/data/formats/fasta.test.js +26 -0
  18. package/src/data/sources/dataSource.js +22 -0
  19. package/src/data/sources/dataSourceFactory.js +24 -0
  20. package/src/data/sources/dataUtils.js +31 -0
  21. package/src/data/sources/dynamicCallbackSource.js +56 -0
  22. package/src/data/sources/dynamicSource.js +36 -0
  23. package/src/data/sources/inlineSource.js +69 -0
  24. package/src/data/sources/inlineSource.test.js +55 -0
  25. package/src/data/sources/namedSource.js +74 -0
  26. package/src/data/sources/sequenceSource.js +46 -0
  27. package/src/data/sources/sequenceSource.test.js +45 -0
  28. package/src/data/sources/urlSource.js +74 -0
  29. package/src/data/transforms/aggregate.js +69 -0
  30. package/src/data/transforms/clone.js +40 -0
  31. package/src/data/transforms/clone.test.js +10 -0
  32. package/src/data/transforms/coverage.js +187 -0
  33. package/src/data/transforms/coverage.test.js +122 -0
  34. package/src/data/transforms/filter.js +37 -0
  35. package/src/data/transforms/filter.test.js +17 -0
  36. package/src/data/transforms/filterScoredLabels.js +134 -0
  37. package/src/data/transforms/flattenCompressedExons.js +57 -0
  38. package/src/data/transforms/flattenDelimited.js +68 -0
  39. package/src/data/transforms/flattenDelimited.test.js +86 -0
  40. package/src/data/transforms/flattenSequence.js +39 -0
  41. package/src/data/transforms/flattenSequence.test.js +33 -0
  42. package/src/data/transforms/formula.js +39 -0
  43. package/src/data/transforms/formula.test.js +18 -0
  44. package/src/data/transforms/identifier.js +108 -0
  45. package/src/data/transforms/identifier.test.js +82 -0
  46. package/src/data/transforms/linearizeGenomicCoordinate.js +101 -0
  47. package/src/data/transforms/measureText.js +44 -0
  48. package/src/data/transforms/pileup.js +128 -0
  49. package/src/data/transforms/pileup.test.js +69 -0
  50. package/src/data/transforms/project.js +41 -0
  51. package/src/data/transforms/project.test.js +31 -0
  52. package/src/data/transforms/regexExtract.js +61 -0
  53. package/src/data/transforms/regexExtract.test.js +66 -0
  54. package/src/data/transforms/regexFold.js +141 -0
  55. package/src/data/transforms/regexFold.test.js +159 -0
  56. package/src/data/transforms/sample.js +101 -0
  57. package/src/data/transforms/sample.test.js +37 -0
  58. package/src/data/transforms/stack.js +137 -0
  59. package/src/data/transforms/stack.test.js +90 -0
  60. package/src/data/transforms/transformFactory.js +60 -0
  61. package/src/encoder/accessor.js +82 -0
  62. package/src/encoder/accessor.test.js +46 -0
  63. package/src/encoder/encoder.js +369 -0
  64. package/src/encoder/encoder.test.js +97 -0
  65. package/src/fonts/Lato-Regular.json +1267 -0
  66. package/src/fonts/Lato-Regular.png +0 -0
  67. package/src/fonts/OFL.txt +93 -0
  68. package/src/fonts/README.md +3 -0
  69. package/src/fonts/bmFont.d.ts +58 -0
  70. package/src/fonts/bmFontManager.js +357 -0
  71. package/src/fonts/bmFontMetrics.js +108 -0
  72. package/src/genome/genome.js +305 -0
  73. package/src/genome/genome.test.js +152 -0
  74. package/src/genome/genomeStore.js +54 -0
  75. package/src/genome/locusFormat.js +31 -0
  76. package/src/genome/scaleIndex.js +199 -0
  77. package/src/genome/scaleIndex.test.js +61 -0
  78. package/src/genome/scaleLocus.js +112 -0
  79. package/src/genome/scaleLocus.test.js +3 -0
  80. package/src/genomeSpy.js +753 -0
  81. package/src/gl/arrayBuilder.js +199 -0
  82. package/src/gl/dataToVertices.js +621 -0
  83. package/src/gl/includes/common.glsl +63 -0
  84. package/src/gl/includes/fp64-arithmetic.glsl +187 -0
  85. package/src/gl/includes/fp64-utils.js +132 -0
  86. package/src/gl/includes/picking.fragment.glsl +3 -0
  87. package/src/gl/includes/picking.vertex.glsl +29 -0
  88. package/src/gl/includes/sampleFacet.glsl +107 -0
  89. package/src/gl/includes/scales.glsl +79 -0
  90. package/src/gl/includes/scales_fp64.glsl +30 -0
  91. package/src/gl/link.fragment.glsl +18 -0
  92. package/src/gl/link.vertex.glsl +111 -0
  93. package/src/gl/point.fragment.glsl +123 -0
  94. package/src/gl/point.vertex.glsl +128 -0
  95. package/src/gl/rect.fragment.glsl +51 -0
  96. package/src/gl/rect.vertex.glsl +114 -0
  97. package/src/gl/rule.fragment.glsl +52 -0
  98. package/src/gl/rule.vertex.glsl +89 -0
  99. package/src/gl/text.fragment.glsl +31 -0
  100. package/src/gl/text.vertex.glsl +246 -0
  101. package/src/gl/webGLHelper.js +490 -0
  102. package/src/img/bowtie.svg +1 -0
  103. package/src/img/genomespy-favicon.svg +34 -0
  104. package/src/index.html +11 -0
  105. package/src/index.js +151 -0
  106. package/src/marks/link.js +189 -0
  107. package/src/marks/mark.js +867 -0
  108. package/src/marks/markUtils.js +109 -0
  109. package/src/marks/pointMark.js +279 -0
  110. package/src/marks/rectMark.js +236 -0
  111. package/src/marks/rule.js +231 -0
  112. package/src/marks/text.js +274 -0
  113. package/src/options.d.ts +9 -0
  114. package/src/scale/colorUtils.js +184 -0
  115. package/src/scale/glslScaleGenerator.js +462 -0
  116. package/src/scale/scale.js +441 -0
  117. package/src/scale/scale.test.js +323 -0
  118. package/src/scale/ticks.js +198 -0
  119. package/src/scale/ticks.test.js +39 -0
  120. package/src/singlePageApp.js +13 -0
  121. package/src/spec/axis.d.ts +296 -0
  122. package/src/spec/channel.d.ts +127 -0
  123. package/src/spec/data.d.ts +185 -0
  124. package/src/spec/font.d.ts +15 -0
  125. package/src/spec/genome.d.ts +35 -0
  126. package/src/spec/mark.d.ts +432 -0
  127. package/src/spec/root.d.ts +22 -0
  128. package/src/spec/scale.d.ts +265 -0
  129. package/src/spec/tooltip.d.ts +9 -0
  130. package/src/spec/transform.d.ts +479 -0
  131. package/src/spec/view.d.ts +215 -0
  132. package/src/styles/genome-spy.scss +153 -0
  133. package/src/tooltip/dataTooltipHandler.js +59 -0
  134. package/src/tooltip/refseqGeneTooltipHandler.js +77 -0
  135. package/src/tooltip/tooltipHandler.ts +12 -0
  136. package/src/types/filetypes.d.ts +4 -0
  137. package/src/types/flatqueue.d.ts +53 -0
  138. package/src/types/glsl.d.ts +4 -0
  139. package/src/types/object.d.ts +21 -0
  140. package/src/types/vega-scale.d.ts +60 -0
  141. package/src/utils/animator.js +83 -0
  142. package/src/utils/arrayUtils.js +55 -0
  143. package/src/utils/binnedRangeIndex.js +83 -0
  144. package/src/utils/clamp.js +8 -0
  145. package/src/utils/cloner.js +32 -0
  146. package/src/utils/cloner.test.js +23 -0
  147. package/src/utils/coalesce.js +11 -0
  148. package/src/utils/coalesce.test.js +15 -0
  149. package/src/utils/concatIterables.js +26 -0
  150. package/src/utils/concatIterables.test.js +7 -0
  151. package/src/utils/debounce.js +37 -0
  152. package/src/utils/domainArray.js +224 -0
  153. package/src/utils/domainArray.test.js +129 -0
  154. package/src/utils/eerp.js +13 -0
  155. package/src/utils/expression.js +32 -0
  156. package/src/utils/field.js +28 -0
  157. package/src/utils/fisheye.js +60 -0
  158. package/src/utils/formatObject.js +31 -0
  159. package/src/utils/html.js +23 -0
  160. package/src/utils/html.test.js +13 -0
  161. package/src/utils/indexer.js +43 -0
  162. package/src/utils/indexer.test.js +46 -0
  163. package/src/utils/inertia.js +124 -0
  164. package/src/utils/interactionEvent.js +33 -0
  165. package/src/utils/iterateNestedMaps.js +21 -0
  166. package/src/utils/iterateNestedMaps.test.js +32 -0
  167. package/src/utils/kWayMerge.js +42 -0
  168. package/src/utils/kWayMerge.test.js +25 -0
  169. package/src/utils/layout/flexLayout.js +336 -0
  170. package/src/utils/layout/flexLayout.test.js +296 -0
  171. package/src/utils/layout/padding.js +107 -0
  172. package/src/utils/layout/point.js +23 -0
  173. package/src/utils/layout/rectangle.js +282 -0
  174. package/src/utils/layout/rectangle.test.js +171 -0
  175. package/src/utils/mergeObjects.js +99 -0
  176. package/src/utils/mergeObjects.test.js +41 -0
  177. package/src/utils/numberExtractor.js +24 -0
  178. package/src/utils/numberExtractor.test.js +5 -0
  179. package/src/utils/point.js +14 -0
  180. package/src/utils/propertyCacher.js +70 -0
  181. package/src/utils/propertyCacher.test.js +84 -0
  182. package/src/utils/propertyCoalescer.js +37 -0
  183. package/src/utils/propertyCoalescer.test.js +21 -0
  184. package/src/utils/reservationMap.js +103 -0
  185. package/src/utils/reservationMap.test.js +19 -0
  186. package/src/utils/scaleNull.js +19 -0
  187. package/src/utils/setOperations.js +75 -0
  188. package/src/utils/smoothstep.js +10 -0
  189. package/src/utils/throttle.js +34 -0
  190. package/src/utils/topK.js +76 -0
  191. package/src/utils/topK.test.js +63 -0
  192. package/src/utils/transition.js +74 -0
  193. package/src/utils/ui/tooltip.js +189 -0
  194. package/src/utils/url.js +22 -0
  195. package/src/utils/variableTools.js +24 -0
  196. package/src/utils/variableTools.test.js +12 -0
  197. package/src/view/axisResolution.js +135 -0
  198. package/src/view/axisResolution.test.js +200 -0
  199. package/src/view/axisView.js +746 -0
  200. package/src/view/channel.js +5 -0
  201. package/src/view/concatView.js +296 -0
  202. package/src/view/containerView.js +141 -0
  203. package/src/view/decoratorView.js +510 -0
  204. package/src/view/facetView.js +488 -0
  205. package/src/view/flowBuilder.js +362 -0
  206. package/src/view/flowBuilder.test.js +124 -0
  207. package/src/view/importView.js +19 -0
  208. package/src/view/layerView.js +60 -0
  209. package/src/view/rendering.d.ts +44 -0
  210. package/src/view/renderingContext/compositeViewRenderingContext.js +51 -0
  211. package/src/view/renderingContext/deferredViewRenderingContext.js +174 -0
  212. package/src/view/renderingContext/layoutRecorderViewRenderingContext.js +128 -0
  213. package/src/view/renderingContext/simpleViewRenderingContext.js +62 -0
  214. package/src/view/renderingContext/svgViewRenderingContext.js +121 -0
  215. package/src/view/renderingContext/viewRenderingContext.js +41 -0
  216. package/src/view/scaleResolution.js +756 -0
  217. package/src/view/scaleResolution.test.js +571 -0
  218. package/src/view/scaleResolutionApi.d.ts +40 -0
  219. package/src/view/testUtils.js +48 -0
  220. package/src/view/unitView.js +368 -0
  221. package/src/view/view.js +589 -0
  222. package/src/view/view.test.js +213 -0
  223. package/src/view/viewContext.d.ts +57 -0
  224. package/src/view/viewFactory.js +179 -0
  225. package/src/view/viewFactory.test.js +16 -0
  226. package/src/view/viewUtils.js +420 -0
@@ -0,0 +1,756 @@
1
+ import {
2
+ panLinear,
3
+ zoomLinear,
4
+ clampRange,
5
+ span,
6
+ panLog,
7
+ zoomLog,
8
+ panPow,
9
+ zoomPow,
10
+ isArray,
11
+ isObject,
12
+ isBoolean,
13
+ } from "vega-util";
14
+ import { isDiscrete, isContinuous } from "vega-scale";
15
+
16
+ import mergeObjects from "../utils/mergeObjects";
17
+ import createScale, { configureScale } from "../scale/scale";
18
+
19
+ import { invalidate, getCachedOrCall } from "../utils/propertyCacher";
20
+ import {
21
+ getChannelDefWithScale,
22
+ getDiscreteRange,
23
+ isColorChannel,
24
+ isDiscreteChannel,
25
+ isPositionalChannel,
26
+ isSecondaryChannel,
27
+ primaryPositionalChannels,
28
+ } from "../encoder/encoder";
29
+ import {
30
+ isChromosomalLocus,
31
+ isChromosomalLocusInterval,
32
+ } from "../genome/genome";
33
+ import { NominalDomain } from "../utils/domainArray";
34
+ import { easeQuadInOut } from "d3-ease";
35
+ import { interpolateZoom } from "d3-interpolate";
36
+ import { shallowArrayEquals } from "../utils/arrayUtils";
37
+
38
+ export const QUANTITATIVE = "quantitative";
39
+ export const ORDINAL = "ordinal";
40
+ export const NOMINAL = "nominal";
41
+ export const LOCUS = "locus"; // Humdum, should this be "genomic"?
42
+ export const INDEX = "index";
43
+
44
+ /**
45
+ * Resolution takes care of merging domains and scales from multiple views.
46
+ * This class also provides some utility methods for zooming the scales etc..
47
+ *
48
+ * TODO: This has grown a bit too fat. Consider splitting.
49
+ *
50
+ * @typedef {import("./scaleResolutionApi").ScaleResolutionApi} ScaleResolutionApi
51
+ * @implements {ScaleResolutionApi}
52
+ *
53
+ * @typedef {{view: import("./unitView").default, channel: Channel}} ResolutionMember
54
+ * @typedef {import("./unitView").default} UnitView
55
+ * @typedef {import("../encoder/encoder").VegaScale} VegaScale
56
+ * @typedef {import("../utils/domainArray").DomainArray} DomainArray
57
+ * @typedef {import("../genome/genome").ChromosomalLocus} ChromosomalLocus
58
+ *
59
+ * @typedef {import("../spec/channel").Channel} Channel
60
+ * @typedef {import("../spec/scale").Scale} Scale
61
+ * @typedef {import("../spec/scale").NumericDomain} NumericDomain
62
+ * @typedef {import("../spec/scale").ScalarDomain} ScalarDomain
63
+ * @typedef {import("../spec/scale").ComplexDomain} ComplexDomain
64
+ * @typedef {import("../spec/scale").ZoomParams} ZoomParams
65
+ */
66
+ export default class ScaleResolution {
67
+ /**
68
+ * @param {Channel} channel
69
+ */
70
+ constructor(channel) {
71
+ this.channel = channel;
72
+ /** @type {ResolutionMember[]} The involved views */
73
+ this.members = [];
74
+ /** @type {string} Data type (quantitative, nominal, etc...) */
75
+ this.type = null;
76
+
77
+ /** @type {number[]} */
78
+ this._zoomExtent = undefined;
79
+
80
+ /** @type {Set<import("./scaleResolutionApi").ScaleResolutionListener>} Observers that are called when the scale domain is changed */
81
+ this._domainListeners = new Set();
82
+
83
+ /** @type {string} An optional unique identifier for the scale */
84
+ this.name = undefined;
85
+
86
+ /** @type {VegaScale} */
87
+ this._scale = undefined;
88
+ }
89
+
90
+ /**
91
+ * Adds a listener that is called when the scale domain is changed,
92
+ * e.g., zoomed. The call is synchronous and happens before the views
93
+ * are rendered.
94
+ *
95
+ * @param {"domain"} type
96
+ * @param {import("./scaleResolutionApi").ScaleResolutionListener} listener function
97
+ */
98
+ addEventListener(type, listener) {
99
+ if (type != "domain") {
100
+ throw new Error("Unsupported event type: " + type);
101
+ }
102
+ this._domainListeners.add(listener);
103
+ }
104
+
105
+ /**
106
+ * @param {"domain"} type
107
+ * @param {import("./scaleResolutionApi").ScaleResolutionListener} listener function
108
+ */
109
+ removeEventListener(type, listener) {
110
+ if (type != "domain") {
111
+ throw new Error("Unsupported event type: " + type);
112
+ }
113
+ this._domainListeners.delete(listener);
114
+ }
115
+
116
+ _notifyDomainListeners() {
117
+ for (const listener of this._domainListeners.values()) {
118
+ listener({
119
+ type: "domain",
120
+ scaleResolution: this,
121
+ });
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Add a view to this resolution.
127
+ * N.B. This is expected to be called in depth-first order
128
+ *
129
+ * @param {UnitView} view
130
+ * @param {import("./view").Channel} channel
131
+ */
132
+ pushUnitView(view, channel) {
133
+ const channelDef = getChannelDefWithScale(view, channel);
134
+ const type = channelDef.type;
135
+ const name = channelDef?.scale?.name;
136
+
137
+ if (name) {
138
+ if (this.name !== undefined && name != this.name) {
139
+ throw new Error(
140
+ `Shared scales have conflicting names: "${name}" vs. "${this.name}"!`
141
+ );
142
+ }
143
+ this.name = name;
144
+ }
145
+
146
+ if (!this.type) {
147
+ this.type = type;
148
+ } else if (type !== this.type && !isSecondaryChannel(channel)) {
149
+ // TODO: Include a reference to the layer
150
+ throw new Error(
151
+ `Can not use shared scale for different data types: ${this.type} vs. ${type}. Use "resolve: independent" for channel ${this.channel}`
152
+ );
153
+ // Actually, point scale could be changed into band scale
154
+ // TODO: Use the same merging logic as in: https://github.com/vega/vega-lite/blob/master/src/scale.ts
155
+ }
156
+
157
+ this.members.push({ view, channel });
158
+ }
159
+
160
+ /**
161
+ * Returns true if the domain has been defined explicitly, i.e. not extracted from the data.
162
+ */
163
+ isExplicitDomain() {
164
+ return !!this.getConfiguredDomain();
165
+ }
166
+
167
+ /**
168
+ * Collects and merges scale properties from the participating views.
169
+ * Does not include inferred default values such as schemes etc.
170
+ *
171
+ * @returns {import("../spec/scale").Scale}
172
+ */
173
+ _getMergedScaleProps() {
174
+ return getCachedOrCall(this, "mergedScaleProps", () => {
175
+ const propArray = this.members
176
+ .map(
177
+ (member) =>
178
+ getChannelDefWithScale(member.view, member.channel)
179
+ .scale
180
+ )
181
+ .filter((props) => props !== undefined);
182
+
183
+ // TODO: Disabled scale: https://vega.github.io/vega-lite/docs/scale.html#disable
184
+ return mergeObjects(propArray, "scale", ["domain"]);
185
+ });
186
+ }
187
+
188
+ /**
189
+ * Returns the merged scale properties supplemented with inferred properties
190
+ * and domain.
191
+ *
192
+ * @returns {import("../spec/scale").Scale}
193
+ */
194
+ getScaleProps() {
195
+ // eslint-disable-next-line complexity
196
+ return getCachedOrCall(this, "scaleProps", () => {
197
+ const mergedProps = this._getMergedScaleProps();
198
+ if (mergedProps === null || mergedProps.type == "null") {
199
+ // No scale (pass-thru)
200
+ // TODO: Check that the channel is compatible
201
+ return { type: "null" };
202
+ }
203
+
204
+ const props = {
205
+ ...this._getDefaultScaleProperties(this.type),
206
+ ...mergedProps,
207
+ };
208
+
209
+ if (!props.type) {
210
+ props.type = getDefaultScaleType(this.channel, this.type);
211
+ }
212
+
213
+ const domain = this.getInitialDomain();
214
+
215
+ if (domain && domain.length > 0) {
216
+ props.domain = domain;
217
+ } else if (isDiscrete(props.type)) {
218
+ props.domain = new NominalDomain();
219
+ }
220
+
221
+ if (!props.domain && props.domainMid !== undefined) {
222
+ // Initialize with a bogus domain so that scale.js can inject the domainMid.
223
+ // The number of domain elements must be know before the glsl scale is generated.
224
+ props.domain = [props.domainMin ?? 0, props.domainMax ?? 1];
225
+ }
226
+
227
+ // Genomic coordinates need higher precision
228
+ if (props.type == LOCUS && !("fp64" in props)) {
229
+ props.fp64 = true;
230
+ }
231
+
232
+ // Reverse discrete y axis
233
+ if (
234
+ this.channel == "y" &&
235
+ isDiscrete(props.type) &&
236
+ props.reverse == undefined
237
+ ) {
238
+ props.reverse = true;
239
+ }
240
+
241
+ if (props.range && props.scheme) {
242
+ delete props.scheme;
243
+ // TODO: Props should be set more intelligently
244
+ /*
245
+ throw new Error(
246
+ `Scale has both "range" and "scheme" defined! Views: ${this._getViewPaths()}`
247
+ );
248
+ */
249
+ }
250
+
251
+ // By default, index and locus scales are zoomable, others are not
252
+ if (!("zoom" in props) && ["index", "locus"].includes(props.type)) {
253
+ props.zoom = true;
254
+ }
255
+
256
+ applyLockedProperties(props, this.channel);
257
+
258
+ return props;
259
+ });
260
+ }
261
+
262
+ getInitialDomain() {
263
+ // TODO: intersect the domain with zoom extent (if it's defined)
264
+ return (
265
+ this.getConfiguredDomain() ??
266
+ (this.type == LOCUS
267
+ ? this.getGenome().getExtent()
268
+ : this.getDataDomain())
269
+ );
270
+ }
271
+
272
+ /**
273
+ * Unions the configured domains of all participating views.
274
+ *
275
+ * @return { DomainArray }
276
+ */
277
+ getConfiguredDomain() {
278
+ return this._reduceDomains((member) =>
279
+ isSecondaryChannel(member.channel)
280
+ ? undefined
281
+ : member.view.getConfiguredDomain(member.channel)
282
+ );
283
+ }
284
+
285
+ /**
286
+ * Extracts and unions the data domains of all participating views.
287
+ *
288
+ * @return { DomainArray }
289
+ */
290
+ getDataDomain() {
291
+ // TODO: Optimize: extract domain only once if the views share the data
292
+ return this._reduceDomains((member) =>
293
+ isSecondaryChannel(member.channel)
294
+ ? undefined
295
+ : member.view.extractDataDomain(member.channel)
296
+ );
297
+ }
298
+
299
+ /**
300
+ * Reconfigures the scale: updates domain and other settings
301
+ */
302
+ reconfigure() {
303
+ if (this._scale && this._scale.type != "null") {
304
+ invalidate(this, "scaleProps");
305
+ const props = this.getScaleProps();
306
+ configureScale(props, this._scale);
307
+ if (isContinuous(this._scale.type)) {
308
+ this._zoomExtent = this._getZoomExtent();
309
+ }
310
+ }
311
+ }
312
+
313
+ /**
314
+ * @returns {import("../encoder/encoder").VegaScale}
315
+ */
316
+ getScale() {
317
+ if (this._scale) {
318
+ return this._scale;
319
+ }
320
+
321
+ const props = this.getScaleProps();
322
+
323
+ const scale = createScale(props);
324
+ this._scale = scale;
325
+
326
+ if (scale.type == "locus") {
327
+ scale.genome(this.getGenome());
328
+ }
329
+
330
+ // Tag the scale and inform encoders and shaders that emulated
331
+ // 64bit floats should be used.
332
+ // N.B. the tag is lost upon scale.clone().
333
+ scale.fp64 = !!props.fp64;
334
+
335
+ if (isContinuous(scale.type)) {
336
+ this._zoomExtent = this._getZoomExtent();
337
+ }
338
+
339
+ return scale;
340
+ }
341
+
342
+ getDomain() {
343
+ return this.getScale().domain();
344
+ }
345
+
346
+ /**
347
+ * @returns {NumericDomain | ComplexDomain}
348
+ */
349
+ getComplexDomain() {
350
+ return (
351
+ this.getGenome()?.toChromosomalInterval(this.getDomain()) ??
352
+ this.getDomain()
353
+ );
354
+ }
355
+
356
+ /**
357
+ * Return true if the scale is zoomable and the current domain differs from the initial domain.
358
+ *
359
+ * @returns true if zoomed
360
+ */
361
+ isZoomed() {
362
+ return (
363
+ this.isZoomable() &&
364
+ shallowArrayEquals(this.getInitialDomain(), this.getDomain())
365
+ );
366
+ }
367
+
368
+ isZoomable() {
369
+ if (!primaryPositionalChannels.includes(this.channel)) {
370
+ return false;
371
+ }
372
+
373
+ const scaleType = this.getScale().type;
374
+ if (
375
+ !["linear", "locus", "index", "log", "pow", "sqrt"].includes(
376
+ scaleType
377
+ )
378
+ ) {
379
+ return false;
380
+ }
381
+
382
+ // Check explicit configuration
383
+ return !!this.getScaleProps().zoom;
384
+ }
385
+
386
+ /**
387
+ * Pans (translates) and zooms using a specified scale factor.
388
+ *
389
+ * @param {number} scaleFactor
390
+ * @param {number} scaleAnchor
391
+ * @param {number} pan
392
+ * @returns {boolean} true if the scale was zoomed
393
+ */
394
+ zoom(scaleFactor, scaleAnchor, pan) {
395
+ if (!this.isZoomable()) {
396
+ return false;
397
+ }
398
+
399
+ const scale = this.getScale();
400
+ const oldDomain = scale.domain();
401
+ let newDomain = [...oldDomain];
402
+
403
+ const anchor = scale.invert(scaleAnchor);
404
+
405
+ if (this.getScaleProps().reverse) {
406
+ pan = -pan;
407
+ }
408
+
409
+ // TODO: log, pow, symlog, ...
410
+ switch (scale.type) {
411
+ case "linear":
412
+ case "index":
413
+ case "locus":
414
+ newDomain = panLinear(newDomain, pan || 0);
415
+ newDomain = zoomLinear(newDomain, anchor, scaleFactor);
416
+ break;
417
+ case "log":
418
+ newDomain = panLog(newDomain, pan || 0);
419
+ newDomain = zoomLog(newDomain, anchor, scaleFactor);
420
+ break;
421
+ case "pow":
422
+ case "sqrt":
423
+ newDomain = panPow(newDomain, pan || 0, scale.exponent());
424
+ newDomain = zoomPow(
425
+ newDomain,
426
+ anchor,
427
+ scaleFactor,
428
+ scale.exponent()
429
+ );
430
+ break;
431
+ default:
432
+ throw new Error("Unsupported scale type: " + scale.type);
433
+ }
434
+
435
+ // TODO: Use the zoomTo method. Move clamping etc there.
436
+ if (this._zoomExtent) {
437
+ newDomain = clampRange(newDomain, ...this._zoomExtent);
438
+ }
439
+
440
+ if ([0, 1].some((i) => newDomain[i] != oldDomain[i])) {
441
+ scale.domain(newDomain);
442
+ this._notifyDomainListeners();
443
+ return true;
444
+ }
445
+
446
+ return false;
447
+ }
448
+
449
+ /**
450
+ * Immediately zooms to the given interval.
451
+ *
452
+ * @param {NumericDomain | ComplexDomain} domain
453
+ * @param {boolean | number} [duration] an approximate duration for transition.
454
+ * Zero duration zooms immediately. Boolean `true` indicates a default duration.
455
+ */
456
+ async zoomTo(domain, duration = false) {
457
+ if (isBoolean(duration)) {
458
+ duration = duration ? 700 : 0;
459
+ }
460
+
461
+ if (!this.isZoomable()) {
462
+ throw new Error("Not a zoomable scale!");
463
+ }
464
+
465
+ const to = this.fromComplexInterval(domain);
466
+
467
+ // TODO: Intersect the domain with zoom extent
468
+
469
+ const animator = this.members[0]?.view.context.animator;
470
+
471
+ const scale = this.getScale();
472
+ const from = /** @type {number[]} */ (scale.domain());
473
+
474
+ if (duration > 0 && from.length == 2) {
475
+ const fw = from[1] - from[0];
476
+ const fc = from[0] + fw / 2;
477
+
478
+ const tw = to[1] - to[0];
479
+ const tc = to[0] + tw / 2;
480
+
481
+ /*
482
+ await animator.transition({
483
+ duration,
484
+ easingFunction: easeExpInOut,
485
+ onUpdate: (t) => {
486
+ const w = eerp(fw, tw, t);
487
+ const wt = (fw - w) / (fw - tw);
488
+ const c = wt * tc + (1 - wt) * fc;
489
+ scale.domain([c - w / 2, c + w / 2]);
490
+ this._notifyDomainListeners();
491
+ },
492
+ });
493
+ */
494
+ const interpolator = interpolateZoom.rho(0.7)(
495
+ [fc, 0, fw],
496
+ [tc, 0, tw]
497
+ );
498
+ await animator.transition({
499
+ duration: (duration / 1000) * interpolator.duration,
500
+ easingFunction: easeQuadInOut,
501
+ onUpdate: (t) => {
502
+ const [c, , w] = interpolator(t);
503
+ scale.domain([c - w / 2, c + w / 2]);
504
+ this._notifyDomainListeners();
505
+ },
506
+ });
507
+ scale.domain(to);
508
+ this._notifyDomainListeners();
509
+ } else {
510
+ scale.domain(to);
511
+ animator?.requestRender();
512
+ this._notifyDomainListeners();
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Returns the zoom level with respect to the reference domain span (the original domain).
518
+ *
519
+ * In principle, this is highly specific to positional channels. However, zooming can
520
+ * be generalized to other quantitative channels such as color, opacity, size, etc.
521
+ */
522
+ getZoomLevel() {
523
+ if (this.isZoomable()) {
524
+ return span(this._zoomExtent) / span(this.getScale().domain());
525
+ }
526
+
527
+ return 1.0;
528
+ }
529
+
530
+ _getZoomExtent() {
531
+ const props = this.getScaleProps();
532
+ const zoom = props.zoom;
533
+
534
+ if (isZoomParams(zoom)) {
535
+ if (isArray(zoom.extent)) {
536
+ return this.fromComplexInterval(zoom.extent);
537
+ }
538
+ }
539
+
540
+ if (zoom) {
541
+ if (props.type == "locus") {
542
+ return this.getGenome().getExtent();
543
+ }
544
+
545
+ // TODO: Perhaps this should be "domain" for index scale and nothing for quantitative.
546
+ // Would behave similarly to Vega-Lite, which doesn't have constraints.
547
+ return this._scale.domain();
548
+ }
549
+ }
550
+
551
+ /**
552
+ * TODO: These actually depend on the mark, so this is clearly a wrong place.
553
+ * And besides, these should be configurable (themeable)
554
+ *
555
+ * @param {string} dataType
556
+ */
557
+ _getDefaultScaleProperties(dataType) {
558
+ const channel = this.channel;
559
+ const props = {};
560
+
561
+ if (this.isExplicitDomain()) {
562
+ props.zero = false;
563
+ }
564
+
565
+ if (isPositionalChannel(channel)) {
566
+ props.nice = !this.isExplicitDomain();
567
+ } else if (isColorChannel(channel)) {
568
+ // TODO: Named ranges
569
+ props.scheme =
570
+ dataType == NOMINAL
571
+ ? "tableau10"
572
+ : dataType == ORDINAL
573
+ ? "blues"
574
+ : "viridis";
575
+ } else if (isDiscreteChannel(channel)) {
576
+ // Shapes of point mark, for example
577
+ props.range = getDiscreteRange(channel);
578
+ } else if (channel == "size") {
579
+ props.range = [0, 400]; // TODO: Configurable default. This is currently optimized for points.
580
+ } else if (channel == "angle") {
581
+ props.range = [0, 360];
582
+ }
583
+
584
+ return props;
585
+ }
586
+
587
+ getGenome() {
588
+ if (this.type !== "locus") {
589
+ return undefined;
590
+ }
591
+
592
+ // TODO: Support multiple assemblies
593
+ const genome = this.members[0].view.context.genomeStore?.getGenome();
594
+ if (!genome) {
595
+ throw new Error("No genome has been defined!");
596
+ }
597
+ return genome;
598
+ }
599
+
600
+ // TODO: Move the "complex" stuff into scaleLocus.
601
+
602
+ /**
603
+ * Inverts a value in range to a value on domain. Returns an object in
604
+ * case of locus scale.
605
+ *
606
+ * @param {number} value
607
+ */
608
+ invertToComplex(value) {
609
+ const scale = this.getScale();
610
+ if ("invert" in scale) {
611
+ const inverted = /** @type {number} */ (scale.invert(value));
612
+ return this.toComplex(inverted);
613
+ } else {
614
+ throw new Error("The scale does not support inverting!");
615
+ }
616
+ }
617
+
618
+ /**
619
+ * @param {number} value
620
+ */
621
+ toComplex(value) {
622
+ const genome = this.getGenome();
623
+ return genome ? genome.toChromosomal(value) : value;
624
+ }
625
+
626
+ /**
627
+ * @param {number | ChromosomalLocus} complex
628
+ * @returns {number}
629
+ */
630
+ fromComplex(complex) {
631
+ if (isChromosomalLocus(complex)) {
632
+ const genome = this.getGenome();
633
+ return genome.toContinuous(complex.chrom, complex.pos);
634
+ }
635
+ return complex;
636
+ }
637
+
638
+ /**
639
+ * @param {ScalarDomain | ComplexDomain} interval
640
+ * @returns {number[]}
641
+ */
642
+ fromComplexInterval(interval) {
643
+ if (this.type === "locus" && isChromosomalLocusInterval(interval)) {
644
+ return this.getGenome().toContinuousInterval(interval);
645
+ }
646
+ return /** @type {number[]} */ (interval);
647
+ }
648
+
649
+ _getViewPaths() {
650
+ return this.members.map((v) => v.view.getPathString()).join(", ");
651
+ }
652
+
653
+ /**
654
+ * Iterate all participanting views and reduce (union) their domains using an accessor.
655
+ * Accessor may return the an explicitly configured domain or a domain extracted from the data.
656
+ *
657
+ * @param {function(ResolutionMember):DomainArray} domainAccessor
658
+ * @returns {DomainArray}
659
+ */
660
+ _reduceDomains(domainAccessor) {
661
+ const domains = this.members
662
+ .map(domainAccessor)
663
+ .filter((domain) => !!domain);
664
+
665
+ if (domains.length) {
666
+ return domains.reduce((acc, curr) => acc.extendAll(curr));
667
+ }
668
+ }
669
+ }
670
+
671
+ /**
672
+ *
673
+ * @param {Channel} channel
674
+ * @param {string} dataType
675
+ */
676
+ function getDefaultScaleType(channel, dataType) {
677
+ // TODO: Band scale, Bin-Quantitative
678
+
679
+ if ([INDEX, LOCUS].includes(dataType)) {
680
+ if (primaryPositionalChannels.includes(channel)) {
681
+ return dataType;
682
+ } else {
683
+ // TODO: Also explicitly set scales should be validated
684
+ throw new Error(
685
+ `${channel} does not support ${dataType} data type. Only positional channels do.`
686
+ );
687
+ }
688
+ }
689
+
690
+ /**
691
+ * @type {Partial<Record<Channel, string[]>>}
692
+ * Default types: nominal, ordinal, quantitative.
693
+ * undefined = incompatible, "null" = disabled (pass-thru)
694
+ */
695
+ const defaults = {
696
+ uniqueId: ["null", undefined, undefined],
697
+ facetIndex: ["null", undefined, undefined],
698
+ x: ["band", "band", "linear"],
699
+ y: ["band", "band", "linear"],
700
+ size: [undefined, "point", "linear"],
701
+ opacity: [undefined, "point", "linear"],
702
+ fillOpacity: [undefined, "point", "linear"],
703
+ strokeOpacity: [undefined, "point", "linear"],
704
+ color: ["ordinal", "ordinal", "linear"],
705
+ fill: ["ordinal", "ordinal", "linear"],
706
+ stroke: ["ordinal", "ordinal", "linear"],
707
+ strokeWidth: [undefined, undefined, "linear"],
708
+ shape: ["ordinal", "ordinal", undefined],
709
+ sample: ["null", "null", undefined],
710
+ semanticScore: [undefined, undefined, "null"],
711
+ search: ["null", undefined, undefined],
712
+ text: ["null", "null", "null"],
713
+ dx: [undefined, undefined, "null"],
714
+ dy: [undefined, undefined, "null"],
715
+ angle: [undefined, undefined, "linear"],
716
+ };
717
+
718
+ const type = defaults[channel]
719
+ ? defaults[channel][[NOMINAL, ORDINAL, QUANTITATIVE].indexOf(dataType)]
720
+ : dataType == QUANTITATIVE
721
+ ? "linear"
722
+ : "ordinal";
723
+
724
+ if (type === undefined) {
725
+ throw new Error(
726
+ `Channel "${channel}" is not compatible with "${dataType}" data type. Use of a proper scale may be needed.`
727
+ );
728
+ }
729
+
730
+ return type;
731
+ }
732
+
733
+ /**
734
+ * @param {import("../spec/scale").Scale} props
735
+ * @param {import("../spec/channel").Channel} channel
736
+ */
737
+ function applyLockedProperties(props, channel) {
738
+ if (isPositionalChannel(channel) && props.type !== "ordinal") {
739
+ props.range = [0, 1];
740
+ }
741
+
742
+ if (channel == "opacity") {
743
+ if (isContinuous(props.type)) {
744
+ props.clamp = true;
745
+ }
746
+ }
747
+ }
748
+
749
+ /**
750
+ *
751
+ * @param {boolean | ZoomParams} zoom
752
+ * @returns {zoom is ZoomParams}
753
+ */
754
+ function isZoomParams(zoom) {
755
+ return isObject(zoom);
756
+ }