@genome-spy/core 0.29.0 → 0.30.2

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