@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,109 @@
1
+ import { isValueDef, getSecondaryChannel } from "../encoder/encoder";
2
+
3
+ /**
4
+ *
5
+ * @typedef {import("../spec/channel").Encoding} Encoding
6
+ * @typedef {import("../spec/channel").Channel} Channel
7
+ */
8
+
9
+ /**
10
+ * @param {Encoding} encoding
11
+ * @param {Channel} channel
12
+ */
13
+ export function fixPositional(encoding, channel) {
14
+ const secondary = getSecondaryChannel(channel);
15
+ if (encoding[channel]) {
16
+ if (!encoding[secondary]) {
17
+ if (encoding[channel].type == "quantitative") {
18
+ // Bar plot, anchor the other end to zero
19
+ encoding[secondary] = {
20
+ datum: 0,
21
+ };
22
+ } else {
23
+ // Must make copies because the definition may be shared with other views/marks
24
+ encoding[channel] = { ...encoding[channel] };
25
+ encoding[secondary] = { ...encoding[channel] };
26
+
27
+ // Fill the bands (bar plot / heatmap)
28
+ // We are following the Vega-Lite convention:
29
+ // the band property works differently on rectangular marks, i.e., it adjusts the band coverage.
30
+ const adjustment = (1 - (encoding[channel].band || 1)) / 2;
31
+ encoding[channel].band = 0 + adjustment;
32
+ encoding[secondary].band = 1 - adjustment;
33
+
34
+ // TODO: If the secondary channel duplicates the primary channel
35
+ // the data should be uploaded to the GPU only once.
36
+ }
37
+ } else if (encoding[channel].type != "quantitative") {
38
+ const adjustment = (1 - (encoding[channel].band || 1)) / 2;
39
+ encoding[channel].band = adjustment;
40
+ encoding[secondary].band = -adjustment;
41
+ }
42
+ } else if (encoding[secondary]) {
43
+ throw new Error(
44
+ `Only secondary channel ${secondary} has been specified!`
45
+ );
46
+ } else {
47
+ // Nothing specified, fill the whole viewport
48
+ encoding[channel] = { value: 0 };
49
+ encoding[secondary] = { value: 1 };
50
+ }
51
+ }
52
+
53
+ /**
54
+ * @param {import("../spec/channel").Encoding} encoding
55
+ * @param {boolean} filled
56
+ */
57
+ export function fixStroke(encoding, filled) {
58
+ if (!encoding.stroke) {
59
+ if (filled) {
60
+ encoding.stroke = { value: null };
61
+ } else {
62
+ encoding.stroke = {
63
+ resolutionChannel: "color",
64
+ ...encoding.color,
65
+ };
66
+ // TODO: Whattabout default strokeWidth?
67
+ }
68
+ }
69
+
70
+ if (isValueDef(encoding.stroke) && encoding.stroke.value === null) {
71
+ encoding.strokeWidth = { value: 0 };
72
+ }
73
+
74
+ if (!encoding.strokeOpacity) {
75
+ encoding.strokeOpacity = {
76
+ resolutionChannel: "opacity",
77
+ ...encoding.opacity,
78
+ };
79
+ }
80
+ }
81
+
82
+ /**
83
+ * @param {import("../spec/channel").Encoding} encoding
84
+ * @param {boolean} filled
85
+ */
86
+ export function fixFill(encoding, filled) {
87
+ if (isValueDef(encoding.fill) && encoding.fill.value === null) {
88
+ encoding.fillOpacity = { value: 0 };
89
+ } else if (!encoding.fill) {
90
+ encoding.fill = {
91
+ resolutionChannel: "color",
92
+ ...encoding.color,
93
+ };
94
+ if (!filled && !encoding.fillOpacity) {
95
+ encoding.fillOpacity = { value: 0 };
96
+ }
97
+ }
98
+
99
+ if (!encoding.fillOpacity) {
100
+ if (filled) {
101
+ encoding.fillOpacity = {
102
+ resolutionChannel: "opacity",
103
+ ...encoding.opacity,
104
+ };
105
+ } else {
106
+ encoding.fillOpacity = { value: 0 };
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,279 @@
1
+ import { drawBufferInfo, setBuffersAndAttributes, setUniforms } from "twgl.js";
2
+ import { bisector, quantileSorted } from "d3-array";
3
+ import { zoomLinear } from "vega-util";
4
+ import { PointVertexBuilder } from "../gl/dataToVertices";
5
+ import VERTEX_SHADER from "../gl/point.vertex.glsl";
6
+ import FRAGMENT_SHADER from "../gl/point.fragment.glsl";
7
+
8
+ import Mark from "./mark";
9
+ import { sampleIterable } from "../data/transforms/sample";
10
+ import { fixFill, fixStroke } from "./markUtils";
11
+
12
+ /** @type {Record<string, import("../view/viewUtils").ChannelDef>} */
13
+ const defaultEncoding = {};
14
+
15
+ export default class PointMark extends Mark {
16
+ /**
17
+ * @param {import("../view/unitView").default} unitView
18
+ */
19
+ constructor(unitView) {
20
+ super(unitView);
21
+
22
+ Object.defineProperties(
23
+ this.defaultProperties,
24
+ Object.getOwnPropertyDescriptors({
25
+ x: 0.5,
26
+ y: 0.5,
27
+ color: "#4c78a8",
28
+ filled: true,
29
+ opacity: 1.0,
30
+ size: 100.0,
31
+ semanticScore: 0.0, // TODO: Should be datum instead of value. But needs fixing.
32
+ shape: "circle",
33
+ strokeWidth: 2.0,
34
+ fillGradientStrength: 0.0,
35
+ dx: 0,
36
+ dy: 0,
37
+ angle: 0,
38
+
39
+ sampleFacetPadding: 0.1,
40
+
41
+ semanticZoomFraction: 0.02,
42
+ })
43
+ );
44
+ }
45
+
46
+ getAttributes() {
47
+ return [
48
+ "inwardStroke",
49
+ "uniqueId",
50
+ "facetIndex",
51
+ "x",
52
+ "y",
53
+ "size",
54
+ "semanticScore",
55
+ "shape",
56
+ "strokeWidth",
57
+ "gradientStrength",
58
+ "dx",
59
+ "dy",
60
+ "fill",
61
+ "stroke",
62
+ "fillOpacity",
63
+ "strokeOpacity",
64
+ "angle",
65
+ ];
66
+ }
67
+
68
+ /**
69
+ * @returns {import("../spec/channel").Channel[]}
70
+ */
71
+ getSupportedChannels() {
72
+ return [
73
+ ...super.getSupportedChannels(),
74
+ "size",
75
+ "semanticScore",
76
+ "shape",
77
+ "strokeWidth",
78
+ "dx",
79
+ "dy",
80
+ "fill",
81
+ "stroke",
82
+ "fillOpacity",
83
+ "strokeOpacity",
84
+ "angle",
85
+ ];
86
+ }
87
+
88
+ getDefaultEncoding() {
89
+ return { ...super.getDefaultEncoding(), ...defaultEncoding };
90
+ }
91
+
92
+ /**
93
+ * @param {import("../spec/channel").Encoding} encoding
94
+ * @returns {import("../spec/channel").Encoding}
95
+ */
96
+ fixEncoding(encoding) {
97
+ fixStroke(encoding, this.properties.filled);
98
+ fixFill(encoding, this.properties.filled);
99
+
100
+ // TODO: Function for getting rid of extras. Also should validate that all attributes are defined
101
+ delete encoding.color;
102
+ delete encoding.opacity;
103
+
104
+ return encoding;
105
+ }
106
+
107
+ initializeData() {
108
+ super.initializeData();
109
+
110
+ // Semantic zooming is currently solely a feature of point mark.
111
+ // Build a sorted sample that allows for computing p-quantiles
112
+ const semanticScoreAccessor =
113
+ this.unitView.getAccessor("semanticScore");
114
+ if (semanticScoreAccessor) {
115
+ // n chosen using Stetson-Harrison
116
+ // TODO: Throw on missing scores
117
+ this.sampledSemanticScores = Float32Array.from(
118
+ sampleIterable(
119
+ 10000,
120
+ this.unitView.getCollector().getData(),
121
+ semanticScoreAccessor
122
+ )
123
+ );
124
+ this.sampledSemanticScores.sort((a, b) => a - b);
125
+ }
126
+ }
127
+
128
+ async initializeGraphics() {
129
+ await super.initializeGraphics();
130
+ this.createAndLinkShaders(VERTEX_SHADER, FRAGMENT_SHADER);
131
+ }
132
+
133
+ finalizeGraphicsInitialization() {
134
+ super.finalizeGraphicsInitialization();
135
+
136
+ this.gl.useProgram(this.programInfo.program);
137
+
138
+ const props = this.properties;
139
+ setUniforms(this.programInfo, {
140
+ uInwardStroke: props.inwardStroke,
141
+ uGradientStrength: props.fillGradientStrength,
142
+ uMaxRelativePointDiameter: 1 - 2 * props.sampleFacetPadding,
143
+ });
144
+ }
145
+
146
+ updateGraphicsData() {
147
+ const collector = this.unitView.getCollector();
148
+ const itemCount = collector.getItemCount();
149
+
150
+ const builder = new PointVertexBuilder({
151
+ encoders: this.encoders,
152
+ attributes: this.getAttributes(),
153
+ numItems: Math.max(itemCount, this.properties.minBufferSize || 0),
154
+ });
155
+
156
+ builder.addBatches(collector.facetBatches);
157
+
158
+ const vertexData = builder.toArrays();
159
+ this.rangeMap = vertexData.rangeMap;
160
+ this.updateBufferInfo(vertexData);
161
+ }
162
+
163
+ _getGeometricScaleFactor() {
164
+ const zoomLevel = Math.pow(2, this.properties.geometricZoomBound || 0);
165
+
166
+ return Math.pow(
167
+ Math.min(1, this.unitView.getZoomLevel() / zoomLevel),
168
+ 1 / 3
169
+ // note: 1/3 appears to yield perceptually more uniform result than 1/2. I don't know why!
170
+ );
171
+ }
172
+
173
+ /**
174
+ * Returns the maximum size of the points in the data, before any scaling
175
+ */
176
+ _getMaxPointSize() {
177
+ const e = this.encoders.size;
178
+ if (e.constant) {
179
+ return e(null);
180
+ } else {
181
+ return e.scale.range().reduce((a, b) => Math.max(a, b));
182
+ }
183
+ }
184
+
185
+ getSemanticThreshold() {
186
+ if (this.sampledSemanticScores) {
187
+ const p = Math.max(
188
+ 0,
189
+ 1 -
190
+ this.properties.semanticZoomFraction *
191
+ this.unitView.getZoomLevel()
192
+ );
193
+ if (p <= 0) {
194
+ // The sampled scores may be missing the min/max values
195
+ return -Infinity;
196
+ } else if (p >= 1) {
197
+ return Infinity;
198
+ } else {
199
+ return quantileSorted(this.sampledSemanticScores, p);
200
+ }
201
+ } else {
202
+ return -1;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * @param {import("../view/rendering").GlobalRenderingOptions} options
208
+ */
209
+ prepareRender(options) {
210
+ super.prepareRender(options);
211
+
212
+ setUniforms(this.programInfo, {
213
+ uMaxPointSize: this._getMaxPointSize(),
214
+ uScaleFactor: this._getGeometricScaleFactor(),
215
+ uSemanticThreshold: this.getSemanticThreshold(),
216
+ });
217
+
218
+ setBuffersAndAttributes(
219
+ this.gl,
220
+ this.programInfo,
221
+ this.vertexArrayInfo
222
+ );
223
+
224
+ // Setup bisector that allows for searching the points that reside within the viewport.
225
+ const xEncoder = this.encoders.x;
226
+ if (xEncoder && !xEncoder.constant) {
227
+ const bisect = bisector(xEncoder.accessor).left;
228
+ const visibleDomain = this.unitView
229
+ .getScaleResolution("x")
230
+ .getScale()
231
+ .domain();
232
+
233
+ // A hack to include points that are just beyond the borders. TODO: Compute based on maxPointSize
234
+ const paddedDomain = zoomLinear(visibleDomain, null, 1.01);
235
+
236
+ /** @param {any[]} facetId */
237
+ this._findIndices = (facetId) => {
238
+ const data = this.unitView
239
+ .getCollector()
240
+ .facetBatches.get(facetId);
241
+
242
+ return [
243
+ bisect(data, paddedDomain[0]),
244
+ bisect(data, paddedDomain[paddedDomain.length - 1]),
245
+ ];
246
+ };
247
+ }
248
+ }
249
+
250
+ /**
251
+ * @param {import("./Mark").MarkRenderingOptions} options
252
+ */
253
+ render(options) {
254
+ const gl = this.gl;
255
+
256
+ return this.createRenderCallback(
257
+ (offset, count) => {
258
+ // TODO: findIndices is rather slow. Consider a more coarse-grained, "tiled" solution.
259
+ const [lower, upper] = this._findIndices
260
+ ? this._findIndices(options.facetId)
261
+ : [0, count];
262
+
263
+ const length = upper - lower;
264
+
265
+ if (length) {
266
+ drawBufferInfo(
267
+ gl,
268
+ this.vertexArrayInfo,
269
+ gl.POINTS,
270
+ length,
271
+ offset + lower
272
+ );
273
+ }
274
+ },
275
+ options,
276
+ () => this.rangeMap
277
+ );
278
+ }
279
+ }
@@ -0,0 +1,236 @@
1
+ import { drawBufferInfo, setBuffersAndAttributes, setUniforms } from "twgl.js";
2
+ import VERTEX_SHADER from "../gl/rect.vertex.glsl";
3
+ import FRAGMENT_SHADER from "../gl/rect.fragment.glsl";
4
+ import { RectVertexBuilder } from "../gl/dataToVertices";
5
+
6
+ import Mark from "./mark";
7
+ import { fixFill, fixPositional, fixStroke } from "./markUtils";
8
+ import { asArray } from "../utils/arrayUtils";
9
+ import { isValueDef } from "../encoder/encoder";
10
+ import { getCachedOrCall } from "../utils/propertyCacher";
11
+
12
+ export default class RectMark extends Mark {
13
+ /**
14
+ * @param {import("../view/unitView").default} unitView
15
+ */
16
+ constructor(unitView) {
17
+ super(unitView);
18
+
19
+ Object.defineProperties(
20
+ this.defaultProperties,
21
+ Object.getOwnPropertyDescriptors({
22
+ x2: undefined,
23
+ y2: undefined,
24
+ filled: true,
25
+ color: "#4c78a8",
26
+ opacity: 1.0,
27
+ strokeWidth: 3,
28
+ cornerRadius: 0.0,
29
+
30
+ minWidth: 0.5, // Minimum width/height prevents annoying flickering when zooming
31
+ minHeight: 0.5,
32
+ minOpacity: 1.0,
33
+
34
+ tessellationZoomThreshold: 10, // This works with genomes, but likely breaks with other data. TODO: Fix, TODO: log2
35
+ tessellationTiles: 35, // TODO: Tiles per unit (bp)
36
+ })
37
+ );
38
+ }
39
+
40
+ getAttributes() {
41
+ return [
42
+ "uniqueId",
43
+ "facetIndex",
44
+ "x",
45
+ "x2",
46
+ "y",
47
+ "y2",
48
+ "fill",
49
+ "stroke",
50
+ "fillOpacity",
51
+ "strokeOpacity",
52
+ "strokeWidth",
53
+ ];
54
+ }
55
+
56
+ /**
57
+ * @returns {import("../spec/channel").Channel[]}
58
+ */
59
+ getSupportedChannels() {
60
+ return [
61
+ ...super.getSupportedChannels(),
62
+ "x2",
63
+ "y2",
64
+ "fill",
65
+ "stroke",
66
+ "fillOpacity",
67
+ "strokeOpacity",
68
+ "strokeWidth",
69
+ ];
70
+ }
71
+
72
+ get opaque() {
73
+ return (
74
+ getCachedOrCall(
75
+ this,
76
+ "opaque",
77
+ () =>
78
+ !this._isRoundedCorners() &&
79
+ !this._isStroked() &&
80
+ isValueDef(this.encoding.fillOpacity) &&
81
+ this.encoding.fillOpacity.value == 1.0 &&
82
+ this.properties.minOpacity == 1.0
83
+ ) && this.unitView.getEffectiveOpacity() == 1
84
+ );
85
+ }
86
+
87
+ /**
88
+ * @param {import("../spec/channel").Encoding} encoding
89
+ * @returns {import("../spec/channel").Encoding}
90
+ */
91
+ fixEncoding(encoding) {
92
+ // TODO: Ensure that both the primary and secondary channel are either variables or constants (values)
93
+ fixPositional(encoding, "x");
94
+ fixPositional(encoding, "y");
95
+
96
+ fixStroke(encoding, this.properties.filled);
97
+ fixFill(encoding, this.properties.filled);
98
+
99
+ // TODO: Function for getting rid of extras. Also should validate that all attributes are defined
100
+ delete encoding.color;
101
+ delete encoding.opacity;
102
+
103
+ return encoding;
104
+ }
105
+
106
+ onBeforeSampleAnimation() {
107
+ // TODO: Tessellate rects inside the viewport
108
+ }
109
+
110
+ onAfterSampleAnimation() {
111
+ // TODO: Pop the previous buffers
112
+ }
113
+
114
+ _isRoundedCorners() {
115
+ return ["", "TopLeft", "TopRight", "BottomLeft", "BottomRight"]
116
+ .map(
117
+ (c) =>
118
+ /** @type {keyof import("../spec/mark").MarkConfig} */ (
119
+ "cornerRadius" + c
120
+ )
121
+ )
122
+ .some((c) => this.properties[c] > 0);
123
+ }
124
+
125
+ _isStroked() {
126
+ const sw = this.encoding.strokeWidth;
127
+ return !(isValueDef(sw) && !sw.value);
128
+ }
129
+
130
+ async initializeGraphics() {
131
+ await super.initializeGraphics();
132
+
133
+ /** @type {string[]} */
134
+ const defines = [];
135
+ if (this._isRoundedCorners()) {
136
+ defines.push("ROUNDED_CORNERS");
137
+ }
138
+ if (this._isStroked()) {
139
+ defines.push("STROKED");
140
+ }
141
+
142
+ this.createAndLinkShaders(
143
+ VERTEX_SHADER,
144
+ FRAGMENT_SHADER,
145
+ defines.map((d) => "#define " + d)
146
+ );
147
+ }
148
+
149
+ updateGraphicsData() {
150
+ const collector = this.unitView.getCollector();
151
+ const numItems = collector.getItemCount();
152
+
153
+ // TODO: Disable tessellation on SimpleTrack - no need for it
154
+ const builder = new RectVertexBuilder({
155
+ encoders: this.encoders,
156
+ attributes: this.getAttributes(),
157
+ numItems,
158
+ buildXIndex: this.properties.buildIndex,
159
+ });
160
+
161
+ builder.addBatches(collector.facetBatches);
162
+
163
+ const vertexData = builder.toArrays();
164
+ this.rangeMap = vertexData.rangeMap;
165
+ this.updateBufferInfo(vertexData);
166
+ }
167
+
168
+ /**
169
+ * @param {import("../view/rendering").GlobalRenderingOptions} options
170
+ */
171
+ prepareRender(options) {
172
+ super.prepareRender(options);
173
+
174
+ const props = this.properties;
175
+
176
+ setUniforms(this.programInfo, {
177
+ uMinSize: [props.minWidth, props.minHeight], // in pixels
178
+ uMinOpacity: props.minOpacity,
179
+ uCornerRadii: [
180
+ props.cornerRadiusTopRight ?? props.cornerRadius,
181
+ props.cornerRadiusBottomRight ?? props.cornerRadius,
182
+ props.cornerRadiusTopLeft ?? props.cornerRadius,
183
+ props.cornerRadiusBottomLeft ?? props.cornerRadius,
184
+ ],
185
+ });
186
+
187
+ setBuffersAndAttributes(
188
+ this.gl,
189
+ this.programInfo,
190
+ this.vertexArrayInfo
191
+ );
192
+ }
193
+
194
+ /**
195
+ * @param {import("./Mark").MarkRenderingOptions} options
196
+ */
197
+ render(options) {
198
+ const gl = this.gl;
199
+
200
+ return this.createRenderCallback(
201
+ (offset, count) => {
202
+ drawBufferInfo(
203
+ gl,
204
+ this.vertexArrayInfo,
205
+ gl.TRIANGLE_STRIP,
206
+ count,
207
+ offset
208
+ );
209
+ },
210
+ options,
211
+ () => this.rangeMap
212
+ );
213
+ }
214
+
215
+ /**
216
+ * Finds a datum that overlaps the given value on the x domain.
217
+ * The result is unspecified if multiple data are found.
218
+ *
219
+ * This is highly specific to SampleView and its sorting/filtering functionality.
220
+ *
221
+ * @param {any} facetId
222
+ * @param {number} x position on the x domain
223
+ * @returns {any}
224
+ */
225
+ findDatumAt(facetId, x) {
226
+ facetId = asArray(facetId); // TODO: Do at the call site
227
+ const e = this.encoders;
228
+ const data = this.unitView.getCollector().facetBatches.get(facetId);
229
+ const a = e.x.accessor;
230
+ const a2 = e.x2.accessor;
231
+ if (data) {
232
+ // TODO: Binary search
233
+ return data.find((d) => x >= a(d) && x < a2(d));
234
+ }
235
+ }
236
+ }