@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,462 @@
1
+ import {
2
+ isContinuous,
3
+ isDiscrete,
4
+ isDiscretizing,
5
+ isInterpolating,
6
+ } from "vega-scale";
7
+ import { fp64ify } from "../gl/includes/fp64-utils";
8
+ import { isArray, isBoolean, isNumber, isString } from "vega-util";
9
+ import { color as d3color } from "d3-color";
10
+
11
+ import {
12
+ getDiscreteRangeMapper,
13
+ isColorChannel,
14
+ isDatumDef,
15
+ isDiscreteChannel,
16
+ getPrimaryChannel,
17
+ } from "../encoder/encoder";
18
+ import { peek } from "../utils/arrayUtils";
19
+
20
+ export const ATTRIBUTE_PREFIX = "attr_";
21
+ export const DOMAIN_PREFIX = "uDomain_";
22
+ export const RANGE_PREFIX = "range_";
23
+ export const SCALE_FUNCTION_PREFIX = "scale_";
24
+ export const SCALED_FUNCTION_PREFIX = "getScaled_";
25
+ export const RANGE_TEXTURE_PREFIX = "uRangeTexture_";
26
+
27
+ // https://stackoverflow.com/a/47543127
28
+ const FLT_MAX = 3.402823466e38;
29
+
30
+ /**
31
+ * @typedef {import("../spec/channel").Channel} Channel
32
+ */
33
+
34
+ /**
35
+ * Splits a vega-scale type (e.g., linear, sequential-linear) to components.
36
+ *
37
+ * @param {string} type
38
+ */
39
+ function splitScaleType(type) {
40
+ const match = type.match(/^(?:(\w+)-)?(\w+)$/);
41
+ if (!match) {
42
+ throw new Error("Not a scale type: " + type);
43
+ }
44
+ return {
45
+ family: match[1] || "continuous",
46
+ transform: match[2],
47
+ };
48
+ }
49
+
50
+ /**
51
+ *
52
+ * @param {Channel} channel
53
+ * @param {number | number[] | string | boolean} value
54
+ */
55
+ export function generateValueGlsl(channel, value) {
56
+ /** @type {VectorizedValue} */
57
+ let vec;
58
+ if (isDiscreteChannel(channel)) {
59
+ vec = vectorize(getDiscreteRangeMapper(channel)(value));
60
+ } else if (isString(value)) {
61
+ if (isColorChannel(channel)) {
62
+ vec = vectorizeCssColor(value);
63
+ } else {
64
+ throw new Error(
65
+ `String values are not supported on the "${channel}" channel: ${value}`
66
+ );
67
+ }
68
+ } else if (isBoolean(value)) {
69
+ vec = vectorize(value ? 1 : 0);
70
+ } else if (value === null) {
71
+ if (isColorChannel(channel)) {
72
+ vec = vectorize([0, 0, 0]);
73
+ } else {
74
+ throw new Error(
75
+ `null value is not supported on the "${channel}" chanel.`
76
+ );
77
+ }
78
+ } else {
79
+ vec = vectorize(value);
80
+ }
81
+
82
+ // These could also be passed as uniforms because GPU drivers often handle
83
+ // uniforms as constants and recompile the shader to eliminate dead code etc.
84
+ let glsl = `
85
+ #define ${channel}_DEFINED
86
+ ${vec.type} ${SCALED_FUNCTION_PREFIX}${channel}() {
87
+ // Constant value
88
+ return ${vec};
89
+ }`;
90
+ return glsl;
91
+ }
92
+
93
+ /**
94
+ *
95
+ * @param {Channel} channel
96
+ * @param {any} scale
97
+ * @param {import("../spec/channel").ChannelDef} encoding
98
+ */
99
+ // eslint-disable-next-line complexity
100
+ export function generateScaleGlsl(channel, scale, encoding) {
101
+ const primary = getPrimaryChannel(channel);
102
+ const attributeName = ATTRIBUTE_PREFIX + channel;
103
+ const domainUniformName = DOMAIN_PREFIX + primary;
104
+ const rangeName = RANGE_PREFIX + primary;
105
+
106
+ const fp64 = !!scale.fp64;
107
+ const attributeType = fp64 ? "vec2" : "float";
108
+
109
+ const domainLength = scale.domain ? scale.domain().length : undefined;
110
+
111
+ /** @type {string} */
112
+ let domainUniform;
113
+
114
+ /** @type {string[]} The generated shader (concatenated at the end) */
115
+ const glsl = [];
116
+
117
+ // For debugging
118
+ glsl.push("");
119
+ glsl.push("/".repeat(70));
120
+ glsl.push(`// Channel: ${channel}`);
121
+ glsl.push("");
122
+
123
+ glsl.push(`#define ${channel}_DEFINED`);
124
+ if (fp64) {
125
+ glsl.push(`#define ${channel}_FP64`);
126
+ }
127
+
128
+ const { transform } = splitScaleType(scale.type);
129
+
130
+ /**
131
+ * @param {string} name
132
+ * @param {...any} args
133
+ */
134
+ const makeScaleCall = (name, ...args) =>
135
+ // eslint-disable-next-line no-useless-call
136
+ makeFunctionCall.apply(null, [
137
+ name + (fp64 ? "Fp64" : ""),
138
+ "value",
139
+ ...args,
140
+ ]);
141
+
142
+ let functionCall;
143
+ switch (transform) {
144
+ case "linear":
145
+ functionCall = makeScaleCall("scaleLinear", "domain", rangeName);
146
+ break;
147
+
148
+ case "log":
149
+ functionCall = makeScaleCall(
150
+ "scaleLog",
151
+ "domain",
152
+ rangeName,
153
+ scale.base()
154
+ );
155
+ break;
156
+
157
+ case "symlog":
158
+ functionCall = makeScaleCall(
159
+ "scaleSymlog",
160
+ "domain",
161
+ rangeName,
162
+ scale.constant()
163
+ );
164
+ break;
165
+
166
+ case "pow":
167
+ case "sqrt":
168
+ functionCall = makeScaleCall(
169
+ "scalePow",
170
+ "domain",
171
+ rangeName,
172
+ scale.exponent()
173
+ );
174
+ break;
175
+
176
+ case "index":
177
+ case "locus":
178
+ case "point":
179
+ case "band":
180
+ functionCall = makeScaleCall(
181
+ "scaleBand",
182
+ "domain",
183
+ rangeName,
184
+ scale.paddingInner(),
185
+ scale.paddingOuter(),
186
+ scale.align(),
187
+ encoding.band ?? 0.5
188
+ );
189
+ break;
190
+
191
+ case "ordinal": // Use identity transform and lookup the value from a texture
192
+ case "null":
193
+ case "identity":
194
+ functionCall = makeScaleCall("scaleIdentity");
195
+ break;
196
+
197
+ case "threshold":
198
+ // TODO: Quantile (it's a specialization of threshold scale)
199
+ // TODO: Quantize
200
+ break;
201
+
202
+ default:
203
+ // TODO: Implement log, sqrt, etc...
204
+ throw new Error(
205
+ `Unsupported scale type: ${
206
+ scale.type
207
+ }! ${channel}: ${JSON.stringify(encoding)}`
208
+ );
209
+ }
210
+
211
+ // N.B. Interpolating scales require unit range
212
+ // TODO: Reverse
213
+ const range =
214
+ isInterpolating(scale.type) ||
215
+ (isContinuous(scale.type) && isColorChannel(channel))
216
+ ? [0, 1]
217
+ : scale.range
218
+ ? scale.range()
219
+ : undefined;
220
+
221
+ if (range && channel == primary && range.length && range.every(isNumber)) {
222
+ const vectorizedRange = vectorizeRange(range);
223
+
224
+ // Range needs no runtime adjustment (at least for now). Thus, pass it as a constant that the
225
+ // GLSL compiler can optimize away in the case of unit ranges.
226
+ glsl.push(
227
+ `const ${vectorizedRange.type} ${rangeName} = ${vectorizedRange};`
228
+ );
229
+ }
230
+
231
+ /** @type {number} */
232
+ let datum;
233
+ if (isDatumDef(encoding)) {
234
+ if (isNumber(encoding.datum)) {
235
+ datum = encoding.datum;
236
+ } else {
237
+ throw new Error(
238
+ `Only quantitative datums are currently supported in the encoding definition: ${JSON.stringify(
239
+ encoding
240
+ )}`
241
+ );
242
+ }
243
+ }
244
+
245
+ const returnType = isColorChannel(channel) ? "vec3" : "float";
246
+
247
+ /**
248
+ * An optional interpolator function that maps the transformed value to the range.
249
+ * @type {string}
250
+ */
251
+ let interpolate;
252
+ if (isColorChannel(channel)) {
253
+ const textureUniformName = RANGE_TEXTURE_PREFIX + primary;
254
+ if (channel == primary) {
255
+ glsl.push(`uniform sampler2D ${textureUniformName};`);
256
+ }
257
+ if (isContinuous(scale.type)) {
258
+ interpolate = `getInterpolatedColor(${textureUniformName}, transformed)`;
259
+ } else if (isDiscrete(scale.type) || isDiscretizing(scale.type)) {
260
+ interpolate = `getDiscreteColor(${textureUniformName}, int(transformed))`;
261
+ } else {
262
+ throw new Error("Problem with color scale!");
263
+ }
264
+ } else if (scale.type === "ordinal" || isDiscretizing(scale.type)) {
265
+ const textureUniformName = RANGE_TEXTURE_PREFIX + primary;
266
+ if (channel == primary) {
267
+ glsl.push(`uniform sampler2D ${textureUniformName};`);
268
+ }
269
+ interpolate = `getDiscreteColor(${textureUniformName}, int(transformed)).r`;
270
+ }
271
+
272
+ // Declare the data: a variable or a constant datum (in domain).
273
+ if (datum !== undefined) {
274
+ // TODO: Datums could also be provided as uniforms, allowing for modifications
275
+ glsl.push(
276
+ `const highp ${attributeType} ${attributeName} = ${vectorize(
277
+ fp64 ? fp64ify(datum) : datum
278
+ )};`
279
+ );
280
+ } else {
281
+ glsl.push(`in highp ${attributeType} ${attributeName};`);
282
+ }
283
+
284
+ /** @type {string[]} Channel's scale function*/
285
+ const scaleBody = [];
286
+
287
+ const piecewise = isContinuous(scale.type) && domainLength > 2;
288
+ const needsSlot = isDiscretizing(scale.type) || piecewise;
289
+
290
+ // 1. If scale is piecewise or discretizing, find a matching slot
291
+ scaleBody.push(`int slot = 0;`);
292
+ if (needsSlot) {
293
+ const name = domainUniformName;
294
+ // Use a simple linear search.
295
+ // This cannot be put into a function because GLSL requires fixed array lengths for parameters.
296
+ scaleBody.push(
297
+ piecewise
298
+ ? `while (slot < ${name}.length() - 2 && value >= ${name}[slot + 1]) { slot++; }`
299
+ : `while (slot < ${name}.length() && value >= ${name}[slot]) { slot++; }`
300
+ );
301
+ }
302
+
303
+ const usesDomain =
304
+ isContinuous(scale.type) ||
305
+ isDiscretizing(scale.type) ||
306
+ ["band", "point"].includes(scale.type);
307
+
308
+ // 2. transform
309
+ if (functionCall) {
310
+ const name = domainUniformName;
311
+ if (usesDomain) {
312
+ const dtype = fp64 ? "vec4" : "vec2";
313
+ scaleBody.push(
314
+ `${dtype} domain = ${dtype}(${name}[slot], ${name}[slot + 1]);`
315
+ );
316
+ }
317
+
318
+ scaleBody.push(`float transformed = ${functionCall};`);
319
+
320
+ if (piecewise) {
321
+ // TODO: Handle range correctly. Now this assumes unit range.
322
+ scaleBody.push(
323
+ `transformed = (float(slot) + transformed) / (float(${name}.length()) - 1.0);`
324
+ );
325
+ }
326
+ } else {
327
+ // Discretizing scale
328
+ scaleBody.push(`float transformed = float(slot);`);
329
+ }
330
+
331
+ // 3. clamp
332
+ if (scale.clamp && scale.clamp()) {
333
+ scaleBody.push(
334
+ `transformed = clampToRange(transformed, ${vectorizeRange(range)});`
335
+ );
336
+ }
337
+
338
+ // 4. interpolate or map to a discrete value
339
+ scaleBody.push(`return ${interpolate ?? "transformed"};`);
340
+
341
+ glsl.push(`
342
+ ${returnType} ${SCALE_FUNCTION_PREFIX}${channel}(${attributeType} value) {
343
+ ${scaleBody.map((x) => ` ${x}\n`).join("")}
344
+ }`);
345
+
346
+ // A convenience getter for the scaled value
347
+ glsl.push(`
348
+ ${returnType} ${SCALED_FUNCTION_PREFIX}${channel}() {
349
+ return ${SCALE_FUNCTION_PREFIX}${channel}(${attributeName});
350
+ }`);
351
+
352
+ const concatenated = glsl.join("\n");
353
+
354
+ if (usesDomain && channel == primary) {
355
+ // Band, point, index, and locus scale need the domain extent (the first and last elements).
356
+ const length =
357
+ isContinuous(scale.type) || isDiscretizing(scale.type)
358
+ ? domainLength
359
+ : 2;
360
+ domainUniform = `${
361
+ fp64 ? "vec2" : "float"
362
+ } ${domainUniformName}[${length}];`;
363
+ }
364
+
365
+ return {
366
+ glsl: concatenated,
367
+ domainUniform,
368
+ };
369
+ }
370
+
371
+ /**
372
+ * Adds a trailing decimal zero so that GLSL is happy.
373
+ *
374
+ * @param {number} number
375
+ * @returns {string}
376
+ */
377
+ function toDecimal(number) {
378
+ if (!isNumber(number)) {
379
+ throw new Error(`Not a number: ${number}`);
380
+ }
381
+
382
+ if (number == Infinity) {
383
+ return "" + FLT_MAX;
384
+ } else if (number == -Infinity) {
385
+ return "" + -FLT_MAX;
386
+ } else {
387
+ let str = `${number}`;
388
+ if (/^(-)?\d+$/.test(str)) {
389
+ str += ".0";
390
+ }
391
+ return str;
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Turns a number or number array to float or vec[234] string.
397
+ *
398
+ * @param {number | number[]} value
399
+ * @returns {VectorizedValue}
400
+ *
401
+ * @typedef {string & { type: string, numComponents: number }} VectorizedValue
402
+ */
403
+ function vectorize(value) {
404
+ if (typeof value == "number") {
405
+ value = [value];
406
+ }
407
+ const numComponents = value.length;
408
+ if (numComponents < 1 || numComponents > 4) {
409
+ throw new Error("Invalid number of components: " + numComponents);
410
+ }
411
+
412
+ let type;
413
+ let str;
414
+
415
+ if (numComponents > 1) {
416
+ type = `vec${numComponents}`;
417
+ str = `${type}(${value.map(toDecimal).join(", ")})`;
418
+ } else {
419
+ type = "float";
420
+ str = toDecimal(value[0]);
421
+ }
422
+
423
+ return Object.assign(str, { type, numComponents });
424
+ }
425
+
426
+ /**
427
+ * @param {string} color
428
+ */
429
+ function vectorizeCssColor(color) {
430
+ const rgb = d3color(color).rgb();
431
+ return vectorize([rgb.r, rgb.g, rgb.b].map((x) => x / 255));
432
+ }
433
+
434
+ /**
435
+ *
436
+ * @param {number[]} range
437
+ */
438
+ function vectorizeRange(range) {
439
+ return vectorize([range[0], peek(range)]);
440
+ }
441
+
442
+ /**
443
+ *
444
+ * @param {string} name
445
+ * @param {...any} args
446
+ */
447
+ function makeFunctionCall(name, ...args) {
448
+ /** @type {string[]} */
449
+ const fixedArgs = [];
450
+
451
+ for (const arg of args) {
452
+ if (isNumber(arg)) {
453
+ fixedArgs.push(toDecimal(arg));
454
+ } else if (isArray(arg)) {
455
+ fixedArgs.push(vectorize(arg));
456
+ } else {
457
+ fixedArgs.push(arg);
458
+ }
459
+ }
460
+
461
+ return `${name}(${fixedArgs.join(", ")})`;
462
+ }