@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,867 @@
1
+ import {
2
+ createBufferInfoFromArrays,
3
+ createProgramInfoFromProgram,
4
+ createUniformBlockInfo,
5
+ createVertexArrayInfo,
6
+ setAttribInfoBufferFromArray,
7
+ setUniformBlock,
8
+ setUniforms,
9
+ } from "twgl.js";
10
+ import { isDiscrete } from "vega-scale";
11
+ import { fp64ify } from "../gl/includes/fp64-utils";
12
+ import createEncoders, {
13
+ isChannelDefWithScale,
14
+ isValueDef,
15
+ } from "../encoder/encoder";
16
+ import {
17
+ DOMAIN_PREFIX,
18
+ generateValueGlsl,
19
+ generateScaleGlsl,
20
+ RANGE_TEXTURE_PREFIX,
21
+ } from "../scale/glslScaleGenerator";
22
+ import FP64 from "../gl/includes/fp64-arithmetic.glsl";
23
+ import GLSL_COMMON from "../gl/includes/common.glsl";
24
+ import GLSL_SCALES from "../gl/includes/scales.glsl";
25
+ import GLSL_SCALES_FP64 from "../gl/includes/scales_fp64.glsl";
26
+ import GLSL_SAMPLE_FACET from "../gl/includes/sampleFacet.glsl";
27
+ import GLSL_PICKING_VERTEX from "../gl/includes/picking.vertex.glsl";
28
+ import GLSL_PICKING_FRAGMENT from "../gl/includes/picking.fragment.glsl";
29
+ import { getCachedOrCall } from "../utils/propertyCacher";
30
+ import { createProgram } from "../gl/webGLHelper";
31
+ import coalesceProperties from "../utils/propertyCoalescer";
32
+
33
+ export const SAMPLE_FACET_UNIFORM = "SAMPLE_FACET_UNIFORM";
34
+ export const SAMPLE_FACET_TEXTURE = "SAMPLE_FACET_TEXTURE";
35
+
36
+ /**
37
+ * @typedef {import("../spec/mark").MarkConfig} MarkConfig
38
+ * @typedef {import("../spec/channel").Channel} Channel
39
+ * @typedef {import("../spec/channel").Encoding} Encoding
40
+ * @typedef {import("../spec/channel").ValueDef} ValueDef
41
+ *
42
+ * @typedef {import("../view/view").RenderingOptions} RenderingOptions
43
+ * @typedef {object} _MarkRenderingOptions
44
+ * @prop {boolean} [skipViewportSetup] Don't configure viewport. Allows for
45
+ * optimized faceted rendering
46
+ * @typedef {RenderingOptions & _MarkRenderingOptions} MarkRenderingOptions
47
+ *
48
+ * @callback DrawFunction
49
+ * @param {number} offset
50
+ * @param {number} count
51
+ *
52
+ */
53
+ export default class Mark {
54
+ /**
55
+ * @param {import("../view/unitView").default} unitView
56
+ */
57
+ constructor(unitView) {
58
+ this.unitView = unitView;
59
+
60
+ /** @type {Record<string, import("../encoder/encoder").Encoder>} */
61
+ this.encoders = undefined;
62
+
63
+ // TODO: Consolidate the following webgl stuff into a single object
64
+
65
+ /** @type {import("twgl.js").BufferInfo & { allocatedVertices?: number }} WebGL buffers */
66
+ this.bufferInfo = undefined;
67
+
68
+ /** @type {import("twgl.js").ProgramInfo} WebGL buffers */
69
+ this.programInfo = undefined;
70
+
71
+ /** @type {import("twgl.js").VertexArrayInfo} WebGL buffers */
72
+ this.vertexArrayInfo = undefined;
73
+
74
+ /** @type {import("twgl.js").UniformBlockInfo} WebGL buffers */
75
+ this.domainUniformInfo = undefined;
76
+
77
+ // TODO: Implement https://vega.github.io/vega-lite/docs/config.html
78
+ /** @type {MarkConfig} */
79
+ this.defaultProperties = {
80
+ get clip() {
81
+ // TODO: Cache once the scales have been resolved
82
+ // TODO: Only check channels that are used
83
+ // TODO: provide more fine-grained xClip and yClip props
84
+ return /** @type {Channel[]} */ (["x", "y"])
85
+ .map((channel) => unitView.getScaleResolution(channel))
86
+ .some((resolution) => resolution?.isZoomable() ?? false);
87
+ },
88
+ xOffset: 0,
89
+ yOffset: 0,
90
+
91
+ /**
92
+ * Minimum size for WebGL buffers (number of data items).
93
+ * Allows for using bufferSubData to update graphics.
94
+ * This property is intended for internal usage.
95
+ */
96
+ minBufferSize: 0,
97
+ };
98
+
99
+ /**
100
+ * A properties object that contains the configured mark properties or
101
+ * default values as fallback.
102
+ *
103
+ * TODO: Proper and comprehensive typings for mark properties
104
+ *
105
+ * @type {Partial<MarkConfig>}
106
+ * @readonly
107
+ */
108
+ this.properties = coalesceProperties(
109
+ typeof this.unitView.spec.mark == "object"
110
+ ? () => /** @type {MarkConfig} */ (this.unitView.spec.mark)
111
+ : () => /** @type {MarkConfig} */ ({}),
112
+ () => this.defaultProperties
113
+ );
114
+ }
115
+
116
+ get opaque() {
117
+ return false;
118
+ }
119
+
120
+ /**
121
+ * Returns attribute info for WebGL attributes that match visual channels.
122
+ *
123
+ * Note: attributes and channels do not necessarily match.
124
+ * For example, rectangles have x, y, x2, and y2 channels but only x and y as attributes.
125
+ *
126
+ * @returns {string[]}
127
+ */
128
+ getAttributes() {
129
+ // override
130
+ throw new Error("Not implemented!");
131
+ }
132
+
133
+ /**
134
+ * @returns {Channel[]}
135
+ */
136
+ getSupportedChannels() {
137
+ return [
138
+ "sample",
139
+ "facetIndex",
140
+ "x",
141
+ "y",
142
+ "color",
143
+ "opacity",
144
+ "search",
145
+ "uniqueId",
146
+ ];
147
+ }
148
+
149
+ /**
150
+ * @returns {Encoding}
151
+ */
152
+ getDefaultEncoding() {
153
+ /** @type {Encoding} */
154
+ const encoding = {
155
+ sample: undefined,
156
+ uniqueId: undefined,
157
+ };
158
+
159
+ if (this.isPickingParticipant()) {
160
+ encoding.uniqueId = {
161
+ field: "_uniqueId", // TODO: Use constant
162
+ type: "nominal",
163
+ scale: null,
164
+ };
165
+ }
166
+
167
+ return encoding;
168
+ }
169
+
170
+ /**
171
+ * Adds intelligent defaults etc to the encoding.
172
+ *
173
+ * @param {Encoding} encoding
174
+ * @returns {Encoding}
175
+ */
176
+ fixEncoding(encoding) {
177
+ return encoding;
178
+ }
179
+
180
+ /**
181
+ * Returns the encoding spec supplemented with mark's default encodings
182
+ *
183
+ * @returns {Encoding}
184
+ */
185
+ get encoding() {
186
+ return getCachedOrCall(this, "encoding", () => {
187
+ const defaults = this.getDefaultEncoding();
188
+ const configured = this.unitView.getEncoding();
189
+
190
+ /** @type {(property: string) => ValueDef} */
191
+ const propToValueDef = (property) => {
192
+ const value =
193
+ this.properties[/** @type {keyof MarkConfig} */ (property)];
194
+ // TODO: Use isScalar ... but... tooltip breaks if type guard is used
195
+ return { value };
196
+ //}
197
+ };
198
+
199
+ const propertyValues = Object.fromEntries(
200
+ this.getSupportedChannels()
201
+ .map(
202
+ (channel) =>
203
+ /** @type {[Channel, ValueDef]} */ ([
204
+ channel,
205
+ propToValueDef(channel),
206
+ ])
207
+ )
208
+ .filter((entry) => entry[1].value !== undefined)
209
+ );
210
+
211
+ const encoding = this.fixEncoding({
212
+ ...defaults,
213
+ ...propertyValues,
214
+ ...configured,
215
+ });
216
+
217
+ for (const channel of Object.keys(encoding)) {
218
+ if (!this.getSupportedChannels().includes(channel)) {
219
+ // TODO: Only delete channels that were inherited
220
+ // Should complain about unsupported channels that were
221
+ // explicitly specified.
222
+ delete encoding[channel];
223
+ }
224
+ }
225
+
226
+ return encoding;
227
+ });
228
+ }
229
+
230
+ getContext() {
231
+ return this.unitView.context;
232
+ }
233
+
234
+ getType() {
235
+ return this.unitView.getMarkType();
236
+ }
237
+
238
+ initializeData() {
239
+ //
240
+ }
241
+
242
+ /**
243
+ * Initialize encoders that encode fields of the data (or constants) to
244
+ * the ranges of the visual channels.
245
+ */
246
+ initializeEncoders() {
247
+ this.encoders = createEncoders(this);
248
+ }
249
+
250
+ /**
251
+ * Initialize shaders etc.
252
+ */
253
+ async initializeGraphics() {
254
+ //override
255
+ }
256
+
257
+ /**
258
+ * Update WebGL buffers from the data
259
+ */
260
+ updateGraphicsData() {
261
+ // override
262
+ }
263
+
264
+ getSampleFacetMode() {
265
+ if (this.encoders.facetIndex) {
266
+ return SAMPLE_FACET_TEXTURE;
267
+ } else if (this.unitView.getFacetAccessor()) {
268
+ return SAMPLE_FACET_UNIFORM;
269
+ }
270
+ }
271
+
272
+ /**
273
+ *
274
+ * @param {string} vertexShader
275
+ * @param {string} fragmentShader
276
+ * @param {string[]} [extraHeaders]
277
+ */
278
+ createAndLinkShaders(vertexShader, fragmentShader, extraHeaders = []) {
279
+ const attributes = this.getAttributes();
280
+
281
+ // For debugging
282
+ extraHeaders.push("// view: " + this.unitView.getPathString());
283
+
284
+ // TODO: This is a temporary variable, don't store it in the mark object
285
+ /** @type {string[]} */
286
+ this.domainUniforms = [];
287
+
288
+ /** @type {string[]} */
289
+ let scaleCode = [];
290
+
291
+ const sampleFacetMode = this.getSampleFacetMode();
292
+ if (sampleFacetMode) {
293
+ extraHeaders.push(`#define ${sampleFacetMode}`);
294
+ }
295
+
296
+ for (const attribute of attributes) {
297
+ /** @type {Channel} */
298
+ let channel;
299
+ if (attribute in this.encoding) {
300
+ channel = /** @type {Channel} */ (attribute);
301
+ } else {
302
+ continue;
303
+ }
304
+
305
+ const channelDef = this.encoding[channel];
306
+
307
+ if (!channelDef) {
308
+ continue;
309
+ }
310
+
311
+ if (isValueDef(channelDef)) {
312
+ scaleCode.push(generateValueGlsl(channel, channelDef.value));
313
+ } else {
314
+ const resolutionChannel =
315
+ (isChannelDefWithScale(channelDef) &&
316
+ channelDef.resolutionChannel) ||
317
+ channel;
318
+
319
+ const scale = this.unitView
320
+ .getScaleResolution(resolutionChannel)
321
+ .getScale();
322
+
323
+ const generated = generateScaleGlsl(channel, scale, channelDef);
324
+
325
+ scaleCode.push(generated.glsl);
326
+ if (generated.domainUniform) {
327
+ this.domainUniforms.push(generated.domainUniform);
328
+ }
329
+ }
330
+ }
331
+
332
+ const domainUniformBlock = this.domainUniforms.length
333
+ ? "layout(std140) uniform Domains {\n" +
334
+ this.domainUniforms.map((u) => ` ${u}\n`).join("") +
335
+ "};\n\n"
336
+ : "";
337
+
338
+ const vertexParts = [
339
+ ...extraHeaders,
340
+ GLSL_COMMON,
341
+ GLSL_SCALES,
342
+ domainUniformBlock,
343
+ ...scaleCode,
344
+ GLSL_SAMPLE_FACET,
345
+ GLSL_PICKING_VERTEX,
346
+ vertexShader,
347
+ ];
348
+
349
+ if (vertexParts.some((code) => /[Ff]p64/.test(code))) {
350
+ vertexParts.unshift(GLSL_SCALES_FP64);
351
+ vertexParts.unshift(FP64);
352
+ }
353
+
354
+ const fragmentParts = [
355
+ ...extraHeaders,
356
+ GLSL_COMMON,
357
+ GLSL_PICKING_FRAGMENT,
358
+ fragmentShader,
359
+ ];
360
+
361
+ const gl = this.gl;
362
+
363
+ // Postpone status checking to allow for background compilation
364
+ // See: https://toji.github.io/shader-perf/
365
+ // TODO: It might make sense to cache and share identical programs between mark instances.
366
+ this.programStatus = createProgram(
367
+ gl,
368
+ this.glHelper.compileShader(gl.VERTEX_SHADER, vertexParts),
369
+ this.glHelper.compileShader(gl.FRAGMENT_SHADER, fragmentParts)
370
+ );
371
+ }
372
+
373
+ /**
374
+ * Check WebGL shader/program compilation/linking status and finalize
375
+ * initialization.
376
+ */
377
+ finalizeGraphicsInitialization() {
378
+ const error = this.programStatus.getProgramErrors();
379
+ if (error) {
380
+ if (error.detail) {
381
+ console.warn(error.detail);
382
+ }
383
+ /** @type {Error & { view?: import("../view/view").default}} */
384
+ const err = new Error(
385
+ "Cannot create shader program: " + error.message
386
+ );
387
+ err.view = this.unitView;
388
+ throw err;
389
+ }
390
+
391
+ this.programInfo = createProgramInfoFromProgram(
392
+ this.gl,
393
+ this.programStatus.program
394
+ );
395
+ delete this.programStatus;
396
+
397
+ if (this.domainUniforms.length) {
398
+ this.domainUniformInfo = createUniformBlockInfo(
399
+ this.gl,
400
+ this.programInfo,
401
+ "Domains"
402
+ );
403
+ }
404
+ }
405
+
406
+ /**
407
+ * Delete WebGL buffers etc.
408
+ */
409
+ deleteGraphicsData() {
410
+ if (this.bufferInfo) {
411
+ const gl = this.gl;
412
+ // A hack to prevent WebGL: INVALID_OPERATION: drawArrays: no buffer is bound to enabled attribute
413
+ // TODO: Consider using bufferSubData or DYNAMIC_DRAW etc...
414
+ for (let i = 0; i < 8; i++) {
415
+ gl.disableVertexAttribArray(i);
416
+ }
417
+
418
+ Object.values(this.bufferInfo.attribs).forEach((attribInfo) =>
419
+ this.gl.deleteBuffer(attribInfo.buffer)
420
+ );
421
+ if (this.bufferInfo.indices) {
422
+ this.gl.deleteBuffer(this.bufferInfo.indices);
423
+ }
424
+ this.bufferInfo = undefined;
425
+ }
426
+ }
427
+
428
+ /**
429
+ *
430
+ * @param {any} vertexData TODO: Extract type from VertexBuilder
431
+ */
432
+ updateBufferInfo(vertexData) {
433
+ // Ensure that no VAOs are inadvertently altered
434
+ this.gl.bindVertexArray(null);
435
+
436
+ if (
437
+ this.bufferInfo &&
438
+ vertexData.vertexCount <= this.bufferInfo.allocatedVertices
439
+ ) {
440
+ for (const [attribute, attributeData] of Object.entries(
441
+ vertexData.arrays
442
+ )) {
443
+ // Skip constants
444
+ if (attributeData.data) {
445
+ // TODO: Check that all attributes and numComponents match
446
+ setAttribInfoBufferFromArray(
447
+ this.gl,
448
+ this.bufferInfo.attribs[attribute],
449
+ attributeData.data,
450
+ 0
451
+ );
452
+ // TODO: Consider double buffering:
453
+ // https://community.khronos.org/t/texture-buffers-are-much-slower-than-uniform-buffers/77139
454
+ }
455
+ }
456
+ } else {
457
+ this.deleteGraphicsData();
458
+ this.bufferInfo = createBufferInfoFromArrays(
459
+ this.gl,
460
+ vertexData.arrays,
461
+ { numElements: vertexData.vertexCount }
462
+ );
463
+ this.bufferInfo.allocatedVertices = vertexData.allocatedVertices;
464
+ this.vertexArrayInfo = undefined;
465
+ }
466
+ }
467
+
468
+ /** Convenience method */
469
+ get glHelper() {
470
+ return this.getContext().glHelper;
471
+ }
472
+
473
+ /** Convenience method */
474
+ get gl() {
475
+ return this.glHelper.gl;
476
+ }
477
+
478
+ onBeforeSampleAnimation() {
479
+ // override
480
+ }
481
+
482
+ onAfterSampleAnimation() {
483
+ // override
484
+ }
485
+
486
+ isReady() {
487
+ return this.bufferInfo && this.programInfo;
488
+ }
489
+
490
+ /**
491
+ * Returns true if this mark instance participates in picking.
492
+ *
493
+ * TODO: Check if tooltip is enabled,
494
+ * TODO: Check if selection (when it's implemented) is enabled
495
+ */
496
+ isPickingParticipant() {
497
+ // TODO: Should check encoding instead
498
+ if (this.properties.tooltip === null) {
499
+ // Disabled
500
+ return false;
501
+ }
502
+
503
+ for (const view of this.unitView.getAncestors()) {
504
+ if (!view.isPickingSupported()) {
505
+ return false;
506
+ }
507
+ }
508
+
509
+ return true;
510
+ }
511
+
512
+ /**
513
+ * Configures the WebGL state for rendering the mark instances.
514
+ * A separate preparation stage allows for efficient rendering of faceted
515
+ * views, i.e., multiple views share the uniforms (such as mark properties
516
+ * and scales) and buffers.
517
+ *
518
+ * @param {import("../view/rendering").GlobalRenderingOptions} options
519
+ */
520
+ // eslint-disable-next-line complexity
521
+ prepareRender(options) {
522
+ const glHelper = this.glHelper;
523
+ const gl = this.gl;
524
+
525
+ if (!this.vertexArrayInfo) {
526
+ this.vertexArrayInfo = createVertexArrayInfo(
527
+ this.gl,
528
+ this.programInfo,
529
+ this.bufferInfo
530
+ );
531
+ }
532
+
533
+ gl.useProgram(this.programInfo.program);
534
+
535
+ if (this.domainUniformInfo) {
536
+ // TODO: Only update the domains that have changed
537
+
538
+ for (const [uniform, setter] of Object.entries(
539
+ this.domainUniformInfo.setters
540
+ )) {
541
+ // TODO: isChannel()
542
+ const channel = /** @type {Channel} */ (
543
+ uniform.substring(DOMAIN_PREFIX.length)
544
+ );
545
+
546
+ const channelDef = this.encoding[channel];
547
+ const resolutionChannel =
548
+ (isChannelDefWithScale(channelDef) &&
549
+ channelDef.resolutionChannel) ||
550
+ channel;
551
+ const resolution =
552
+ this.unitView.getScaleResolution(resolutionChannel);
553
+
554
+ if (resolution) {
555
+ const scale = resolution.getScale();
556
+ const domain = isDiscrete(scale.type)
557
+ ? [0, scale.domain().length]
558
+ : scale.domain();
559
+
560
+ setter(
561
+ scale.fp64
562
+ ? domain.map((x) => fp64ify(x)).flat()
563
+ : domain
564
+ );
565
+ }
566
+ }
567
+
568
+ setUniformBlock(gl, this.programInfo, this.domainUniformInfo);
569
+ }
570
+
571
+ for (const [channel, channelDef] of Object.entries(this.encoding)) {
572
+ if (isChannelDefWithScale(channelDef)) {
573
+ const resolutionChannel =
574
+ (isChannelDefWithScale(channelDef) &&
575
+ channelDef.resolutionChannel) ||
576
+ channel;
577
+
578
+ const resolution =
579
+ this.unitView.getScaleResolution(resolutionChannel);
580
+
581
+ const texture = glHelper.rangeTextures.get(resolution);
582
+ if (texture) {
583
+ setUniforms(this.programInfo, {
584
+ [RANGE_TEXTURE_PREFIX + channel]: texture,
585
+ });
586
+ }
587
+ }
588
+ }
589
+
590
+ if (this.getSampleFacetMode() == SAMPLE_FACET_TEXTURE) {
591
+ /** @type {WebGLTexture} */
592
+ let facetTexture;
593
+ for (const view of this.unitView.getAncestors()) {
594
+ facetTexture = view.getSampleFacetTexture();
595
+ if (facetTexture) {
596
+ break;
597
+ }
598
+ }
599
+
600
+ setUniforms(this.programInfo, {
601
+ uSampleFacetTexture: facetTexture,
602
+ });
603
+ }
604
+
605
+ setUniforms(this.programInfo, {
606
+ ONE: 1.0, // a hack needed by emulated 64 bit floats
607
+ uDevicePixelRatio: this.glHelper.dpr,
608
+ uViewOpacity: this.unitView.getEffectiveOpacity(),
609
+ // TODO: Rendering of the mark should be completely skipped if it doesn't
610
+ // participate picking
611
+ uPickingEnabled:
612
+ (options.picking ?? false) && this.isPickingParticipant(),
613
+ });
614
+
615
+ setUniforms(this.programInfo, {
616
+ // left pos, left height, right pos, right height
617
+ uSampleFacet: [0, 1, 0, 1],
618
+ uTransitionOffset: 0.0,
619
+ });
620
+
621
+ if (this.opaque || options.picking) {
622
+ gl.disable(gl.BLEND);
623
+ } else {
624
+ gl.enable(gl.BLEND);
625
+ }
626
+ }
627
+
628
+ /**
629
+ * Prepares rendering of a single sample facet.
630
+ *
631
+ * @param {MarkRenderingOptions} options
632
+ * @returns {boolean} true if rendering should proceed,
633
+ * false if it should be skipped
634
+ */
635
+ prepareSampleFacetRendering(options) {
636
+ const opts = options.sampleFacetRenderingOptions;
637
+ const locationSetter = this.programInfo.uniformSetters.uSampleFacet;
638
+
639
+ if (opts && locationSetter) {
640
+ const pos = opts.locSize ? opts.locSize.location : 0.0;
641
+ const height = opts.locSize ? opts.locSize.size : 1.0;
642
+
643
+ if (pos > 1.0 || pos + height < 0.0) {
644
+ // Not visible
645
+ return false;
646
+ }
647
+
648
+ const targetPos = opts.targetLocSize
649
+ ? opts.targetLocSize.location
650
+ : pos;
651
+ const targetHeight = opts.targetLocSize
652
+ ? opts.targetLocSize.size
653
+ : height;
654
+
655
+ // Use WebGL directly, because twgl uses gl.uniform4fv, which has an
656
+ // inferior performance. Based on profiling, this optimization gives
657
+ // a significant performance boost.
658
+ this.gl.uniform4f(
659
+ locationSetter.location, // TODO: Make a twgl pull request to fix typing
660
+ pos,
661
+ height,
662
+ targetPos,
663
+ targetHeight
664
+ );
665
+ }
666
+
667
+ return true;
668
+ }
669
+
670
+ /**
671
+ * Returns a callback function that the ViewRenderingContext calls to
672
+ * perform the actual rendering either immediately or at a later time.
673
+ *
674
+ * @param {MarkRenderingOptions} options
675
+ * @returns {function():void} A function that renderingContext calls to
676
+ * trigger the actual rendering
677
+ */
678
+ render(options) {
679
+ // Override
680
+ return undefined;
681
+ }
682
+
683
+ /**
684
+ * @param {DrawFunction} draw A function that draws a range of vertices
685
+ * @param {import("./Mark").MarkRenderingOptions} options
686
+ * @param {function():Map<string, import("../gl/dataToVertices").RangeEntry>} rangeMapSource
687
+ */
688
+ createRenderCallback(draw, options, rangeMapSource) {
689
+ // eslint-disable-next-line consistent-this
690
+ const self = this;
691
+
692
+ /** @type {function(import("../gl/dataToVertices").RangeEntry):void} rangeEntry */
693
+ let drawWithRangeEntry;
694
+
695
+ if (this.properties.buildIndex) {
696
+ const scale = this.unitView.getScaleResolution("x")?.getScale();
697
+
698
+ drawWithRangeEntry = (rangeEntry) => {
699
+ if (scale && rangeEntry.xIndex) {
700
+ const domain = scale.domain();
701
+ const vertexIndices = rangeEntry.xIndex(
702
+ domain[0],
703
+ domain[1]
704
+ );
705
+ const offset = vertexIndices[0];
706
+ const count = vertexIndices[1] - offset;
707
+ if (count > 0) {
708
+ draw(offset, count);
709
+ }
710
+ } else {
711
+ draw(rangeEntry.offset, rangeEntry.count);
712
+ }
713
+ };
714
+ } else {
715
+ drawWithRangeEntry = (rangeEntry) =>
716
+ draw(rangeEntry.offset, rangeEntry.count);
717
+ }
718
+
719
+ if (this.properties.dynamicData) {
720
+ return function renderDynamic() {
721
+ const rangeEntry = rangeMapSource().get(options.facetId);
722
+ if (rangeEntry && rangeEntry.count) {
723
+ if (self.prepareSampleFacetRendering(options)) {
724
+ drawWithRangeEntry(rangeEntry);
725
+ }
726
+ }
727
+ };
728
+ } else {
729
+ const rangeEntry = rangeMapSource().get(options.facetId);
730
+ if (rangeEntry && rangeEntry.count) {
731
+ return function renderStatic() {
732
+ if (self.prepareSampleFacetRendering(options)) {
733
+ drawWithRangeEntry(rangeEntry);
734
+ }
735
+ };
736
+ }
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Sets viewport, clipping, and uniforms related to scaling and translation
742
+ *
743
+ * @param {import("../utils/layout/rectangle").default} coords
744
+ * @param {import("../utils/layout/rectangle").default} [clipRect]
745
+ * @returns {boolean} true if the viewport is renderable (size > 0)
746
+ */
747
+ setViewport(coords, clipRect) {
748
+ const dpr = this.glHelper.dpr;
749
+ const gl = this.gl;
750
+ const props = this.properties;
751
+
752
+ const logicalSize = this.glHelper.getLogicalCanvasSize();
753
+
754
+ // Translate by half a pixel to place vertical / horizontal
755
+ // rules inside pixels, not between pixels.
756
+ const pixelOffset = 0.5;
757
+
758
+ // Note: we also handle xOffset/yOffset mark properties here
759
+ const xOffset = (props.xOffset || 0) + pixelOffset;
760
+ const yOffset = (props.yOffset || 0) + pixelOffset;
761
+
762
+ /** @type {object} */
763
+ let uniforms;
764
+
765
+ let clippedCoords = coords;
766
+
767
+ if (props.clip || clipRect) {
768
+ let xClipOffset = 0;
769
+ let yClipOffset = 0;
770
+
771
+ /** @type {[number, number]} */
772
+ let uViewScale;
773
+
774
+ if (clipRect) {
775
+ clippedCoords = props.clip
776
+ ? coords.intersect(clipRect)
777
+ : clipRect;
778
+
779
+ uViewScale = [
780
+ coords.width / clippedCoords.width,
781
+ coords.height / clippedCoords.height,
782
+ ];
783
+
784
+ yClipOffset = Math.max(0, coords.y2 - clipRect.y2);
785
+ xClipOffset = Math.max(0, coords.x2 - clipRect.x2); // TODO: Check sign
786
+ } else {
787
+ uViewScale = [1, 1];
788
+ }
789
+
790
+ const physicalGlCoords = [
791
+ coords.x,
792
+ logicalSize.height - clippedCoords.y2,
793
+ Math.max(0, clippedCoords.width),
794
+ Math.max(0, clippedCoords.height),
795
+ ].map((x) => x * dpr);
796
+
797
+ // Because glViewport accepts only integers, we subtract the rounding
798
+ // errors from xyOffsets to guarantee that graphics in clipped
799
+ // and non-clipped viewports align correctly
800
+ const flooredCoords = physicalGlCoords.map((x) => Math.floor(x));
801
+ const [xError, yError] = physicalGlCoords.map(
802
+ (x, i) => x - flooredCoords[i]
803
+ );
804
+
805
+ // @ts-ignore
806
+ gl.viewport(...flooredCoords);
807
+ // @ts-ignore
808
+ gl.scissor(...flooredCoords);
809
+ gl.enable(gl.SCISSOR_TEST);
810
+
811
+ uniforms = {
812
+ uViewOffset: [
813
+ (xOffset + xClipOffset + xError) / clippedCoords.width,
814
+ -(yOffset + yClipOffset - yError) / clippedCoords.height,
815
+ ],
816
+ uViewScale,
817
+ };
818
+ } else {
819
+ // Viewport comprises of the full canvas
820
+ gl.viewport(
821
+ 0,
822
+ 0,
823
+ logicalSize.width * dpr,
824
+ logicalSize.height * dpr
825
+ );
826
+ gl.disable(gl.SCISSOR_TEST);
827
+
828
+ // Offset and scale all drawing to the view rectangle
829
+ uniforms = {
830
+ uViewOffset: [
831
+ (coords.x + xOffset) / logicalSize.width,
832
+ (logicalSize.height - coords.y - yOffset - coords.height) /
833
+ logicalSize.height,
834
+ ],
835
+ uViewScale: [
836
+ coords.width / logicalSize.width,
837
+ coords.height / logicalSize.height,
838
+ ],
839
+ };
840
+ }
841
+
842
+ // TODO: Optimization: Use uniform buffer object
843
+ setUniforms(this.programInfo, uniforms);
844
+
845
+ setUniforms(this.programInfo, {
846
+ uViewportSize: [coords.width, coords.height],
847
+ });
848
+
849
+ // TODO: Optimize: don't set viewport and stuff if rect is outside clipRect or screen
850
+
851
+ return clippedCoords.height > 0 && clippedCoords.width > 0;
852
+ }
853
+
854
+ /**
855
+ * Finds a datum that overlaps the given value on the x domain.
856
+ * The result is unspecified if multiple data are found.
857
+ *
858
+ * This is highly specific to SampleView and its sorting/filtering functionality.
859
+ *
860
+ * @param {string} facetId
861
+ * @param {number} x position on the x domain
862
+ * @returns {any}
863
+ */
864
+ findDatumAt(facetId, x) {
865
+ // override
866
+ }
867
+ }