@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,40 @@
1
+ import createCloner from "../../utils/cloner";
2
+ import FlowNode, { BEHAVIOR_CLONES, isFileBatch } from "../flowNode";
3
+
4
+ /**
5
+ * Clones the data objects that pass through.
6
+ */
7
+ export default class CloneTransform extends FlowNode {
8
+ get behavior() {
9
+ return BEHAVIOR_CLONES;
10
+ }
11
+
12
+ constructor() {
13
+ super();
14
+
15
+ /** @param {any} datum */
16
+ const setupCloner = (datum) => {
17
+ const clone = createCloner(datum);
18
+
19
+ /** @param {any} datum */
20
+ this.handle = (datum) => this._propagate(clone(datum));
21
+
22
+ this.handle(datum);
23
+ };
24
+
25
+ this.handle = setupCloner;
26
+
27
+ /**
28
+ * Signals that a new batch of data will be propagated.
29
+ *
30
+ * @param {import("../flowNode").FlowBatch} [flowBatch]
31
+ */
32
+ this.beginBatch = (flowBatch) => {
33
+ if (isFileBatch(flowBatch)) {
34
+ // TODO: Only create new cloner if the props change
35
+ this.handle = setupCloner;
36
+ }
37
+ super.beginBatch(flowBatch);
38
+ };
39
+ }
40
+ }
@@ -0,0 +1,10 @@
1
+ import { processData } from "../flowTestUtils";
2
+ import CloneTransform from "./clone";
3
+
4
+ test("CloneTransform clones the data objects", () => {
5
+ const data = [{ x: 1 }, { x: 2 }];
6
+ const clonedData = processData(new CloneTransform(), data);
7
+
8
+ expect(clonedData).toEqual(data);
9
+ expect(clonedData[0]).not.toBe(data[0]);
10
+ });
@@ -0,0 +1,187 @@
1
+ import FlatQueue from "flatqueue";
2
+
3
+ import { field } from "../../utils/field";
4
+ import FlowNode, { BEHAVIOR_CLONES } from "../flowNode";
5
+
6
+ /**
7
+ * @typedef {import("../../spec/transform").CoverageParams} CoverageParams
8
+ */
9
+
10
+ /**
11
+ * Computes coverage for sorted segments
12
+ *
13
+ * TODO: Binned coverage
14
+ */
15
+ export default class CoverageTransform extends FlowNode {
16
+ get behavior() {
17
+ return BEHAVIOR_CLONES;
18
+ }
19
+
20
+ /**
21
+ * @param {CoverageParams} params
22
+ */
23
+ constructor(params) {
24
+ super();
25
+ this.params = params;
26
+
27
+ this.startAccessor = field(params.start);
28
+ this.endAccessor = field(params.end);
29
+
30
+ /** @type {function(any):string} */
31
+ this.chromAccessor = params.chrom
32
+ ? field(params.chrom)
33
+ : (d) => undefined;
34
+ /** @type {function(any):number} */
35
+ this.weightAccessor = params.weight ? field(params.weight) : (d) => 1;
36
+
37
+ this.as = {
38
+ coverage: params.as || "coverage",
39
+ start: params.asStart || params.start,
40
+ end: params.asEnd || params.end,
41
+ chrom: params.asChrom || params.chrom,
42
+ };
43
+
44
+ // eslint-disable-next-line no-new-func
45
+ this.createSegment = /** @type {function} */ (
46
+ new Function(
47
+ "start",
48
+ "end",
49
+ "coverage",
50
+ "chrom",
51
+ "return {" +
52
+ Object.entries(this.as)
53
+ .filter(([param, prop]) => prop)
54
+ .map(
55
+ ([param, prop]) =>
56
+ `${JSON.stringify(prop)}: ${param}`
57
+ )
58
+ .join(", ") +
59
+ "};"
60
+ )
61
+ );
62
+
63
+ // End pos as priority, weight as value
64
+ this.ends = new FlatQueue();
65
+ }
66
+
67
+ reset() {
68
+ super.reset();
69
+ this.initialize();
70
+ }
71
+
72
+ initialize() {
73
+ const asCoverage = this.as.coverage;
74
+ const asEnd = this.as.end;
75
+ const asChrom = this.as.chrom;
76
+
77
+ const startAccessor = this.startAccessor;
78
+ const endAccessor = this.endAccessor;
79
+ const chromAccessor = this.chromAccessor;
80
+ const weightAccessor = this.weightAccessor;
81
+
82
+ /** @type {Record<string, number|string>} used for merging adjacent segment */
83
+ let bufferedSegment;
84
+
85
+ /** @type {string} */
86
+ let prevChrom;
87
+
88
+ /** @type {string} */
89
+ let chrom;
90
+
91
+ // TODO: Whattabout cumulative error when float weights are used?
92
+ // Howabout https://github.com/d3/d3-array#fsum ?
93
+ let coverage = 0;
94
+
95
+ /** @type {number} */
96
+ let prevEdge;
97
+
98
+ // End pos as priority, weight as value
99
+ const ends = this.ends;
100
+ ends.clear();
101
+
102
+ /**
103
+ * @param {number} start
104
+ * @param {number} end
105
+ * @param {number} coverage
106
+ */
107
+ const pushSegment = (start, end, coverage) => {
108
+ if (start == end) {
109
+ return;
110
+ }
111
+
112
+ let extended = false;
113
+ if (bufferedSegment) {
114
+ if (bufferedSegment[asCoverage] === coverage) {
115
+ // Extend it
116
+ bufferedSegment[asEnd] = end;
117
+ extended = true;
118
+ } else if (bufferedSegment[asCoverage] != 0) {
119
+ this._propagate(bufferedSegment);
120
+ }
121
+ }
122
+
123
+ if (!extended) {
124
+ bufferedSegment = this.createSegment(
125
+ start,
126
+ end,
127
+ coverage,
128
+ chrom
129
+ );
130
+ }
131
+ };
132
+
133
+ const flushQueue = () => {
134
+ // Flush queue
135
+ /** @type {number} */
136
+ let edge;
137
+ while ((edge = ends.peekValue()) !== undefined) {
138
+ pushSegment(prevEdge, edge, coverage);
139
+ prevEdge = edge;
140
+ coverage -= ends.pop();
141
+ }
142
+ prevEdge = undefined;
143
+
144
+ if (bufferedSegment) {
145
+ this._propagate(bufferedSegment);
146
+ bufferedSegment = undefined;
147
+ }
148
+ };
149
+
150
+ /** @param {Record<string, any>} datum */
151
+ this.handle = (datum) => {
152
+ const start = startAccessor(datum);
153
+
154
+ /** @type {number} */
155
+ let edge;
156
+ while ((edge = ends.peekValue()) !== undefined && edge < start) {
157
+ pushSegment(prevEdge, edge, coverage);
158
+ prevEdge = edge;
159
+ coverage -= ends.pop();
160
+ }
161
+
162
+ if (asChrom) {
163
+ let newChrom = chromAccessor(datum);
164
+ if (newChrom !== prevChrom) {
165
+ flushQueue();
166
+ chrom = newChrom;
167
+ prevChrom = chrom;
168
+ }
169
+ }
170
+
171
+ if (prevEdge !== undefined) {
172
+ pushSegment(prevEdge, start, coverage);
173
+ }
174
+ prevEdge = start;
175
+
176
+ const weight = weightAccessor(datum);
177
+ coverage += weight;
178
+
179
+ ends.push(weight, endAccessor(datum));
180
+ };
181
+
182
+ this.complete = () => {
183
+ flushQueue();
184
+ super.complete();
185
+ };
186
+ }
187
+ }
@@ -0,0 +1,122 @@
1
+ import CoverageTransform from "./coverage";
2
+ import { processData } from "../flowTestUtils";
3
+
4
+ /**
5
+ * @typedef {import("../../spec/transform").CoverageParams} CoverageParams
6
+ */
7
+
8
+ /**
9
+ * @param {CoverageParams} params
10
+ * @param {any[]} data
11
+ */
12
+ function transform(params, data) {
13
+ const t = new CoverageTransform(params);
14
+ t.initialize();
15
+ return processData(t, data);
16
+ }
17
+
18
+ test("Coverage transform produces correct coverage segments", () => {
19
+ const reads = [
20
+ [0, 4],
21
+ [1, 3],
22
+ [2, 6],
23
+ [4, 8],
24
+ [8, 10],
25
+ [11, 14],
26
+ [11, 13],
27
+ [11, 12],
28
+ [15, 18],
29
+ [16, 18],
30
+ [17, 18],
31
+ ].map((d) => ({
32
+ start: d[0],
33
+ end: d[1],
34
+ }));
35
+
36
+ const coverageSegments = [
37
+ [0, 1, 1],
38
+ [1, 2, 2],
39
+ [2, 3, 3],
40
+ [3, 6, 2],
41
+ [6, 10, 1],
42
+ [11, 12, 3],
43
+ [12, 13, 2],
44
+ [13, 14, 1],
45
+ [15, 16, 1],
46
+ [16, 17, 2],
47
+ [17, 18, 3],
48
+ ].map((d) => ({
49
+ start: d[0],
50
+ end: d[1],
51
+ coverage: d[2],
52
+ }));
53
+
54
+ /** @type {CoverageParams} */
55
+ const coverageConfig = {
56
+ type: "coverage",
57
+ start: "start",
58
+ end: "end",
59
+ };
60
+ expect(transform(coverageConfig, reads)).toEqual(coverageSegments);
61
+ });
62
+
63
+ test("Coverage transform handles chromosomes", () => {
64
+ const reads = [
65
+ { chrom: "chr1", start: 0, end: 1 },
66
+ { chrom: "chr2", start: 0, end: 1 },
67
+ { chrom: "chr3", start: 1, end: 3 },
68
+ ];
69
+
70
+ const coverageSegments = [
71
+ { chrom: "chr1", start: 0, end: 1, coverage: 1 },
72
+ { chrom: "chr2", start: 0, end: 1, coverage: 1 },
73
+ { chrom: "chr3", start: 1, end: 3, coverage: 1 },
74
+ ];
75
+
76
+ /** @type {CoverageParams} */
77
+ const coverageConfig = {
78
+ type: "coverage",
79
+ chrom: "chrom",
80
+ start: "start",
81
+ end: "end",
82
+ };
83
+
84
+ expect(transform(coverageConfig, reads)).toEqual(coverageSegments);
85
+ });
86
+
87
+ test("Coverage transform handles weights", () => {
88
+ const reads = [
89
+ [0, 4, 1],
90
+ [1, 3, 2],
91
+ [2, 6, 3],
92
+ [8, 10, -1],
93
+ ].map((d) => ({
94
+ start: d[0],
95
+ end: d[1],
96
+ weight: d[2],
97
+ }));
98
+
99
+ const coverageSegments = [
100
+ [0, 1, 1],
101
+ [1, 2, 3],
102
+ [2, 3, 6],
103
+ [3, 4, 4],
104
+ [4, 6, 3],
105
+ [8, 10, -1],
106
+ ].map((d) => ({
107
+ start: d[0],
108
+ end: d[1],
109
+ coverage: d[2],
110
+ }));
111
+
112
+ /** @type {CoverageParams} */
113
+ const coverageConfig = {
114
+ type: "coverage",
115
+ chrom: "chrom",
116
+ start: "start",
117
+ end: "end",
118
+ weight: "weight",
119
+ };
120
+
121
+ expect(transform(coverageConfig, reads)).toEqual(coverageSegments);
122
+ });
@@ -0,0 +1,37 @@
1
+ import createFunction from "../../utils/expression";
2
+ import FlowNode from "../flowNode";
3
+
4
+ /**
5
+ * @typedef {import("../../spec/transform").FilterParams} FilterParams
6
+ */
7
+
8
+ export default class FilterTransform extends FlowNode {
9
+ /**
10
+ *
11
+ * @param {FilterParams} params
12
+ */
13
+ constructor(params) {
14
+ super();
15
+ this.params = params;
16
+
17
+ /** @type {(datum: any) => boolean} */
18
+ this.predicate = undefined;
19
+ }
20
+
21
+ initialize() {
22
+ this.predicate = createFunction(
23
+ this.params.expr,
24
+ this.getGlobalObject()
25
+ );
26
+ }
27
+
28
+ /**
29
+ *
30
+ * @param {import("../flowNode").Datum} datum
31
+ */
32
+ handle(datum) {
33
+ if (this.predicate(datum)) {
34
+ this._propagate(datum);
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,17 @@
1
+ import { processData } from "../flowTestUtils";
2
+ import FilterTransform from "./filter";
3
+
4
+ test("FilterTransform filter rows", () => {
5
+ const data = [1, 2, 3, 4, 5, 6].map((x) => ({ x }));
6
+
7
+ /** @type {import("../../spec/transform").FilterParams} */
8
+ const filterParams = {
9
+ type: "filter",
10
+ expr: "datum.x > 3 && datum.x != 5",
11
+ };
12
+
13
+ const t = new FilterTransform(filterParams);
14
+ t.initialize();
15
+
16
+ expect(processData(t, data)).toEqual([4, 6].map((x) => ({ x })));
17
+ });
@@ -0,0 +1,134 @@
1
+ import { bisector } from "d3-array";
2
+ import FlowNode from "../flowNode";
3
+ import { topKSlice } from "../../utils/topK";
4
+ import ReservationMap from "../../utils/reservationMap";
5
+ import { field } from "../../utils/field";
6
+
7
+ /**
8
+ * @typedef {import("../../spec/transform").FilterScoredLabelsParams} Params
9
+ * @typedef {import("../../view/view").default} View
10
+ */
11
+ export default class FilterScoredLabelsTransform extends FlowNode {
12
+ /**
13
+ *
14
+ * @param {Params} params
15
+ * @param {View} view
16
+ */
17
+ constructor(params, view) {
18
+ super();
19
+
20
+ this.params = params;
21
+
22
+ /** @type {any[]} */
23
+ this._data = [];
24
+
25
+ this.channel = params.channel ?? "x";
26
+
27
+ if (!["x", "y"].includes(this.channel)) {
28
+ throw new Error("Invalid channel: " + this.channel);
29
+ }
30
+
31
+ this.posAccessor = field(this.params.pos);
32
+ this.posBisector = bisector(this.posAccessor);
33
+ this.scoreAccessor = field(this.params.score);
34
+ this.widthAccessor = field(this.params.width);
35
+ /** @type {function(any):any} */
36
+ this.laneAccessor = this.params.lane
37
+ ? field(this.params.lane)
38
+ : (d) => 0;
39
+ this.padding = this.params.padding ?? 0;
40
+
41
+ /** @type {Map<any, ReservationMap>} */
42
+ this.reservationMaps = new Map();
43
+
44
+ this.resolution = view.getScaleResolution(this.channel);
45
+
46
+ // Synchronize propagation with rendering because we need both the domain and the range (length of the axis).
47
+ const callback = () => this._filterAndPropagate();
48
+ this.schedule = () => view.context.animator.requestTransition(callback);
49
+
50
+ // Propagate when the domain changes
51
+ this.resolution.addEventListener("domain", (scale) => this.schedule());
52
+
53
+ // Propagate when layout changes. Abusing a "private" method.
54
+ // TODO: Provide another attachment point, in view context for example
55
+ view._addBroadcastHandler("layoutComputed", () => this.schedule());
56
+
57
+ // TODO: Remove observers when this FlowNode is thrown away.
58
+ }
59
+
60
+ complete() {
61
+ const posAccessor = this.posAccessor;
62
+ this._data.sort((a, b) => posAccessor(a) - posAccessor(b));
63
+
64
+ this._scores = this._data.map(this.scoreAccessor);
65
+
66
+ for (const lane of new Set(this._data.map(this.laneAccessor))) {
67
+ this.reservationMaps.set(lane, new ReservationMap(200));
68
+ }
69
+
70
+ this.schedule();
71
+
72
+ super.complete();
73
+ }
74
+
75
+ _filterAndPropagate() {
76
+ super.reset();
77
+
78
+ const scale = this.resolution.getScale();
79
+ const rangeSpan =
80
+ this.resolution.members[0].view.coords?.[
81
+ this.channel == "x" ? "width" : "height"
82
+ ];
83
+ if (!rangeSpan) {
84
+ // The view size is not (yet) available
85
+ return;
86
+ }
87
+
88
+ for (const reservationMap of this.reservationMaps.values()) {
89
+ reservationMap.reset();
90
+ }
91
+
92
+ const domain = scale.domain();
93
+ const k = 70; // TODO: Configurable
94
+
95
+ // Find the maximum of k elements from the visible domain in priority order
96
+ const topIndices = topKSlice(
97
+ this._scores,
98
+ k,
99
+ this.posBisector.left(this._data, domain[0]),
100
+ this.posBisector.right(this._data, domain[1])
101
+ );
102
+
103
+ // Try to fit the elements on the available lanes and propagate if there was room
104
+ for (const i of topIndices) {
105
+ const datum = this._data[i];
106
+ const pos = scale(this.posAccessor(datum)) * rangeSpan;
107
+ const halfWidth = this.widthAccessor(datum) / 2 + this.padding;
108
+
109
+ if (
110
+ this.reservationMaps
111
+ .get(this.laneAccessor(datum))
112
+ .reserve(pos - halfWidth, pos + halfWidth)
113
+ ) {
114
+ this._propagate(datum);
115
+ }
116
+ }
117
+
118
+ super.complete();
119
+ }
120
+
121
+ reset() {
122
+ super.reset();
123
+ this._data = [];
124
+ this.groups = new Map();
125
+ }
126
+
127
+ /**
128
+ *
129
+ * @param {import("../flowNode").Datum} datum
130
+ */
131
+ handle(datum) {
132
+ this._data.push(datum);
133
+ }
134
+ }
@@ -0,0 +1,57 @@
1
+ import { field } from "../../utils/field";
2
+ import numberExtractor from "../../utils/numberExtractor";
3
+ import FlowNode, { BEHAVIOR_CLONES } from "../flowNode";
4
+
5
+ /**
6
+ * @typedef {import("../../spec/transform").FlattenCompressedExonsParams} FlattenCompressedExonsParams
7
+ */
8
+
9
+ /**
10
+ * Flattens "run-length encoded" exons. The transforms inputs the start
11
+ * coordinate of the gene body and a comma-delimited string of alternating
12
+ * intron and exon lengths. A new datum is created for each exon.
13
+ */
14
+ export default class FlattenCompressedExonsTransform extends FlowNode {
15
+ get behavior() {
16
+ return BEHAVIOR_CLONES;
17
+ }
18
+
19
+ /**
20
+ *
21
+ * @param {FlattenCompressedExonsParams} params
22
+ */
23
+ constructor(params) {
24
+ super();
25
+
26
+ const exonsAccessor = field(params.exons ?? "exons");
27
+ const startAccessor = field(params.start ?? "start");
28
+ const [exonStart, exonEnd] = params.as || ["exonStart", "exonEnd"];
29
+
30
+ /**
31
+ *
32
+ * @param {any} datum
33
+ */
34
+ this.handle = (datum) => {
35
+ let upper = startAccessor(datum);
36
+ let lower = upper;
37
+
38
+ let inExon = true;
39
+ const exons = exonsAccessor(datum);
40
+ for (const token of numberExtractor(exons)) {
41
+ if (inExon) {
42
+ lower = upper + token;
43
+ } else {
44
+ upper = lower + token;
45
+
46
+ const newRow = Object.assign({}, datum);
47
+ newRow[exonStart] = lower;
48
+ newRow[exonEnd] = upper;
49
+
50
+ this._propagate(newRow);
51
+ }
52
+
53
+ inExon = !inExon;
54
+ }
55
+ };
56
+ }
57
+ }
@@ -0,0 +1,68 @@
1
+ import { asArray } from "../../utils/arrayUtils";
2
+ import { field } from "../../utils/field";
3
+ import FlowNode, { BEHAVIOR_CLONES } from "../flowNode";
4
+
5
+ /**
6
+ * @typedef {import("../../spec/transform").FlattenDelimitedParams} FlattenDelimitedParams
7
+ * @prop {string[]} separators
8
+ * @prop {string[]} fields
9
+ * @prop {string[]} [as]
10
+ */
11
+
12
+ export default class FlattenDelimitedTransform extends FlowNode {
13
+ get behavior() {
14
+ return BEHAVIOR_CLONES;
15
+ }
16
+
17
+ /**
18
+ *
19
+ * @param {FlattenDelimitedParams} params
20
+ */
21
+ constructor(params) {
22
+ super();
23
+
24
+ // TODO: Validate config. string elements, etc...
25
+
26
+ const accessors = asArray(params.field).map((f) => field(f));
27
+ const separators = asArray(params.separator);
28
+ const as = asArray(params.as || params.field);
29
+
30
+ if (
31
+ accessors.length !== separators.length ||
32
+ accessors.length !== as.length
33
+ ) {
34
+ throw new Error(
35
+ `Lengths of "separator" (${separators.length}), "fields" (${accessors.length}), and "as" (${as.length}) do not match!`
36
+ );
37
+ }
38
+
39
+ /** @param {any[]} datum */
40
+ this.handle = (datum) => {
41
+ if (accessors.some((a) => !a(datum))) return;
42
+
43
+ const splitFields = accessors.map((accessor, i) =>
44
+ accessor(datum).split(separators[i])
45
+ );
46
+ validateSplit(splitFields, datum);
47
+ const flatLen = splitFields[0].length;
48
+
49
+ for (let ri = 0; ri < flatLen; ri++) {
50
+ const newRow = Object.assign({}, datum);
51
+ for (let fi = 0; fi < accessors.length; fi++) {
52
+ newRow[as[fi]] = splitFields[fi][ri];
53
+ }
54
+ this._propagate(newRow);
55
+ }
56
+ };
57
+ }
58
+ }
59
+
60
+ function validateSplit(splitFields, row) {
61
+ const splitLengths = splitFields.map((f) => f.length);
62
+ if (!splitLengths.every((x) => x == splitLengths[0])) {
63
+ throw new Error(
64
+ "Mismatching number of elements in the fields to be split: " +
65
+ JSON.stringify(row)
66
+ );
67
+ }
68
+ }