@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,86 @@
1
+ import { processData } from "../flowTestUtils";
2
+ import FlattenDelimitedTransform from "./flattenDelimited";
3
+
4
+ const sampleData = [
5
+ { id: 1, a: "q, w, e", b: "a-s-d" },
6
+ { id: 2, a: "r, t, y", b: "f-g-h" },
7
+ { id: 3, a: "u", b: "j" },
8
+ ];
9
+
10
+ /**
11
+ * @param {import("./flattenDelimited").FlattenDelimitedParams} params
12
+ * @param {any[]} data
13
+ */
14
+ function transform(params, data) {
15
+ return processData(new FlattenDelimitedTransform(params), data);
16
+ }
17
+
18
+ describe("FlattenDelimited transform", () => {
19
+ test("With a single field", () => {
20
+ /** @type {import("./flattenDelimited").FlattenDelimitedParams} */
21
+ const config = {
22
+ type: "flattenDelimited",
23
+ field: "a",
24
+ separator: ", ",
25
+ };
26
+
27
+ expect(transform(config, sampleData)).toEqual([
28
+ { id: 1, a: "q", b: "a-s-d" },
29
+ { id: 1, a: "w", b: "a-s-d" },
30
+ { id: 1, a: "e", b: "a-s-d" },
31
+ { id: 2, a: "r", b: "f-g-h" },
32
+ { id: 2, a: "t", b: "f-g-h" },
33
+ { id: 2, a: "y", b: "f-g-h" },
34
+ { id: 3, a: "u", b: "j" },
35
+ ]);
36
+ });
37
+
38
+ test("With two fields", () => {
39
+ /** @type {import("./flattenDelimited").FlattenDelimitedParams} */
40
+ const config = {
41
+ type: "flattenDelimited",
42
+ field: ["a", "b"],
43
+ as: ["a", "c"],
44
+ separator: [", ", "-"],
45
+ };
46
+
47
+ expect(transform(config, sampleData)).toEqual([
48
+ { id: 1, a: "q", b: "a-s-d", c: "a" },
49
+ { id: 1, a: "w", b: "a-s-d", c: "s" },
50
+ { id: 1, a: "e", b: "a-s-d", c: "d" },
51
+ { id: 2, a: "r", b: "f-g-h", c: "f" },
52
+ { id: 2, a: "t", b: "f-g-h", c: "g" },
53
+ { id: 2, a: "y", b: "f-g-h", c: "h" },
54
+ { id: 3, a: "u", b: "j", c: "j" },
55
+ ]);
56
+ });
57
+
58
+ test("Throws on differing field lengths", () => {
59
+ const data = [
60
+ {
61
+ a: "1-2",
62
+ b: "1-2-3",
63
+ },
64
+ ];
65
+
66
+ /** @type {import("./flattenDelimited").FlattenDelimitedParams} */
67
+ const config = {
68
+ type: "flattenDelimited",
69
+ field: ["a", "b"],
70
+ separator: ["-", "-"],
71
+ };
72
+
73
+ expect(() => transform(config, data)).toThrow();
74
+ });
75
+
76
+ test("Throws on mismatching spec lengths", () => {
77
+ /** @type {import("./flattenDelimited").FlattenDelimitedParams} */
78
+ const config = {
79
+ type: "flattenDelimited",
80
+ field: ["a", "b"],
81
+ separator: ["a"],
82
+ };
83
+
84
+ expect(() => transform(config, sampleData)).toThrow();
85
+ });
86
+ });
@@ -0,0 +1,39 @@
1
+ import { field } from "../../utils/field";
2
+ import FlowNode, { BEHAVIOR_CLONES } from "../flowNode";
3
+
4
+ /**
5
+ * @typedef {import("../../spec/transform").FlattenSequenceParams} FlattenSequenceParams
6
+ */
7
+
8
+ export default class FlattenSequenceTransform extends FlowNode {
9
+ get behavior() {
10
+ return BEHAVIOR_CLONES;
11
+ }
12
+
13
+ /**
14
+ *
15
+ * @param {FlattenSequenceParams} params
16
+ */
17
+ constructor(params) {
18
+ super();
19
+
20
+ const accessor = field(params.field ?? "sequence");
21
+ const [asPos, asSequence] = params.as ?? ["pos", "sequence"];
22
+
23
+ /** @param {any[]} datum */
24
+ this.handle = (datum) => {
25
+ // TODO: Use code generation
26
+ const template = Object.assign({}, datum, {
27
+ [asSequence]: "",
28
+ [asPos]: 0,
29
+ });
30
+ const sequence = /** @type {string} */ (accessor(datum));
31
+ for (let i = 0; i < sequence.length; i++) {
32
+ const newObject = Object.assign({}, template);
33
+ newObject[asPos] = i;
34
+ newObject[asSequence] = sequence.charAt(i);
35
+ this._propagate(newObject);
36
+ }
37
+ };
38
+ }
39
+ }
@@ -0,0 +1,33 @@
1
+ import { processData } from "../flowTestUtils";
2
+ import FlattenSequenceTransform from "./flattenSequence";
3
+
4
+ /**
5
+ * @param {import("./flattenSequence").FlattenSequenceParams} params
6
+ * @param {any[]} data
7
+ */
8
+ function transform(params, data) {
9
+ return processData(new FlattenSequenceTransform(params), data);
10
+ }
11
+
12
+ test("FlattenSequenece", () => {
13
+ expect(
14
+ transform(
15
+ {
16
+ type: "flattenSequence",
17
+ field: "seq",
18
+ as: ["p", "seq"],
19
+ },
20
+ [
21
+ { identifier: "A", seq: "TCG" },
22
+ { identifier: "B", seq: "AAT" },
23
+ ]
24
+ )
25
+ ).toEqual([
26
+ { identifier: "A", seq: "T", p: 0 },
27
+ { identifier: "A", seq: "C", p: 1 },
28
+ { identifier: "A", seq: "G", p: 2 },
29
+ { identifier: "B", seq: "A", p: 0 },
30
+ { identifier: "B", seq: "A", p: 1 },
31
+ { identifier: "B", seq: "T", p: 2 },
32
+ ]);
33
+ });
@@ -0,0 +1,39 @@
1
+ import createFunction from "../../utils/expression";
2
+ import FlowNode, { BEHAVIOR_MODIFIES } from "../flowNode";
3
+
4
+ /**
5
+ * @typedef {import("../../spec/transform").FormulaParams} FormulaParams
6
+ */
7
+
8
+ export default class FormulaTransform extends FlowNode {
9
+ get behavior() {
10
+ return BEHAVIOR_MODIFIES;
11
+ }
12
+
13
+ /**
14
+ *
15
+ * @param {FormulaParams} params
16
+ */
17
+ constructor(params) {
18
+ super();
19
+ this.params = params;
20
+
21
+ this.as = params.as;
22
+
23
+ /** @type {(datum: any) => any} */
24
+ this.fn = undefined;
25
+ }
26
+
27
+ initialize() {
28
+ this.fn = createFunction(this.params.expr, this.getGlobalObject());
29
+ }
30
+
31
+ /**
32
+ *
33
+ * @param {import("../flowNode").Datum} datum
34
+ */
35
+ handle(datum) {
36
+ datum[this.as] = this.fn(datum);
37
+ this._propagate(datum);
38
+ }
39
+ }
@@ -0,0 +1,18 @@
1
+ import { processData } from "../flowTestUtils";
2
+ import FormulaTransform from "./formula";
3
+
4
+ test("FormulaTransform", () => {
5
+ const data = [{ a: 2 }, { a: 3 }];
6
+
7
+ const t = new FormulaTransform({
8
+ type: "formula",
9
+ expr: "datum.a * 2",
10
+ as: "b",
11
+ });
12
+ t.initialize();
13
+
14
+ expect(processData(t, data)).toEqual([
15
+ { a: 2, b: 4 },
16
+ { a: 3, b: 6 },
17
+ ]);
18
+ });
@@ -0,0 +1,108 @@
1
+ import FlowNode, { BEHAVIOR_MODIFIES } from "../flowNode";
2
+
3
+ export const DEFAULT_AS = "_uniqueId";
4
+
5
+ export const BLOCK_SIZE = 10000;
6
+
7
+ /**
8
+ * TODO: The reservation map should be bound to GenomeSpy instances.
9
+ * Because it's now global, there's a higher risk that we run out of ids.
10
+ *
11
+ * TODO: Identifier transforms should be removed from the reservation map
12
+ * when a transform is removed from the flow.
13
+ *
14
+ * The first block is reserved for "none".
15
+ *
16
+ * @type {IdentifierTransform[]}
17
+ */
18
+ const reservationMap = [null];
19
+
20
+ /**
21
+ * Assigns unique identifiers for tuples that pass through this transform.
22
+ *
23
+ * The identifiers are reserved in equally sized blocks, allowing for
24
+ * quick lookup of the IdentifierTransform instance that assigned the id.
25
+ * This is mainly used for creating ids that can be used for picking, i.e.,
26
+ * selecting rendered data items by hovering or clicking.
27
+ *
28
+ * @typedef {import("../../spec/transform").IdentifierParams} IdentifierParams
29
+ */
30
+ export default class IdentifierTransform extends FlowNode {
31
+ get behavior() {
32
+ return BEHAVIOR_MODIFIES;
33
+ }
34
+
35
+ /**
36
+ *
37
+ * @param {IdentifierParams} params
38
+ */
39
+ constructor(params) {
40
+ super();
41
+ this.params = params;
42
+
43
+ this.as = params.as ?? DEFAULT_AS;
44
+
45
+ /**
46
+ * The block indexes reserved by this transform instance.
47
+ * @type {number[]}
48
+ */
49
+ this._blocks = [];
50
+
51
+ /**
52
+ * The number of blocks used
53
+ */
54
+ this._usedBlocks = 0;
55
+
56
+ /**
57
+ * The next advancement allocates the initial block for this instance
58
+ */
59
+ this._id = -1;
60
+ }
61
+
62
+ initialize() {
63
+ //
64
+ }
65
+
66
+ reset() {
67
+ super.reset();
68
+
69
+ this._usedBlocks = 0;
70
+ this._id = -1;
71
+ }
72
+
73
+ /**
74
+ *
75
+ * @param {import("../flowNode").Datum} datum
76
+ */
77
+ handle(datum) {
78
+ datum[this.as] = this._nextId();
79
+ this._propagate(datum);
80
+ }
81
+
82
+ /**
83
+ * @returns {number}
84
+ */
85
+ _nextId() {
86
+ if (++this._id % BLOCK_SIZE == 0) {
87
+ this._id = this._getBlock() * BLOCK_SIZE;
88
+ }
89
+ return this._id;
90
+ }
91
+
92
+ _getBlock() {
93
+ if (this._usedBlocks < this._blocks.length) {
94
+ return this._blocks[this._usedBlocks++];
95
+ }
96
+
97
+ return this._reserveBlock();
98
+ }
99
+
100
+ _reserveBlock() {
101
+ const blockId = reservationMap.length;
102
+ reservationMap[blockId] = this;
103
+ this._blocks.push(blockId);
104
+ this._usedBlocks++;
105
+
106
+ return blockId;
107
+ }
108
+ }
@@ -0,0 +1,82 @@
1
+ import { range } from "d3-array";
2
+ import { processData } from "../flowTestUtils";
3
+ import IdentifierTransform, { BLOCK_SIZE, DEFAULT_AS } from "./identifier";
4
+
5
+ test("An IdentifierTransform adds identifiers correctly", () => {
6
+ const data = range(BLOCK_SIZE * 2).map((x) => ({ data: x }));
7
+
8
+ const identifiedData = processData(
9
+ new IdentifierTransform({ type: "identifier" }),
10
+ data
11
+ );
12
+
13
+ // The fist block is skipped
14
+ const firstId = BLOCK_SIZE;
15
+
16
+ expect(identifiedData[0]).toEqual({ data: 0, [DEFAULT_AS]: firstId });
17
+ expect(identifiedData[1]).toEqual({ data: 1, [DEFAULT_AS]: firstId + 1 });
18
+ expect(identifiedData[BLOCK_SIZE]).toEqual({
19
+ data: BLOCK_SIZE,
20
+ [DEFAULT_AS]: firstId + BLOCK_SIZE,
21
+ });
22
+ expect(identifiedData[BLOCK_SIZE + 1]).toEqual({
23
+ data: BLOCK_SIZE + 1,
24
+ [DEFAULT_AS]: firstId + BLOCK_SIZE + 1,
25
+ });
26
+ });
27
+
28
+ test("Another transform instance adds identifiers correctly", () => {
29
+ const data = range(BLOCK_SIZE * 2).map((x) => ({ data: x }));
30
+ // Another instance
31
+ const identifiedData = processData(
32
+ new IdentifierTransform({ type: "identifier" }),
33
+ data
34
+ );
35
+
36
+ // The fist block was skipped and the previous test case consumed two blocks
37
+ const firstId = BLOCK_SIZE * 3;
38
+
39
+ expect(identifiedData[0]).toEqual({ data: 0, [DEFAULT_AS]: firstId });
40
+ expect(identifiedData[1]).toEqual({ data: 1, [DEFAULT_AS]: firstId + 1 });
41
+ expect(identifiedData[BLOCK_SIZE]).toEqual({
42
+ data: BLOCK_SIZE,
43
+ [DEFAULT_AS]: firstId + BLOCK_SIZE,
44
+ });
45
+ expect(identifiedData[BLOCK_SIZE + 1]).toEqual({
46
+ data: BLOCK_SIZE + 1,
47
+ [DEFAULT_AS]: firstId + BLOCK_SIZE + 1,
48
+ });
49
+ });
50
+
51
+ test("IdentifierTransform recycles allocated blocks", () => {
52
+ let data = range(BLOCK_SIZE * 2).map((x) => ({ data: x }));
53
+
54
+ const transform = new IdentifierTransform({ type: "identifier" });
55
+ let identifiedData = processData(transform, data);
56
+
57
+ let firstId = BLOCK_SIZE * 5;
58
+
59
+ expect(identifiedData[0]).toEqual({ data: 0, [DEFAULT_AS]: firstId });
60
+ expect(identifiedData[BLOCK_SIZE]).toEqual({
61
+ data: BLOCK_SIZE,
62
+ [DEFAULT_AS]: firstId + BLOCK_SIZE,
63
+ });
64
+
65
+ data = range(BLOCK_SIZE * 3).map((x) => ({ data: x }));
66
+
67
+ // Resetting the transform. It should now reuse the allocated blocks.
68
+ transform.reset();
69
+ identifiedData = processData(transform, data);
70
+
71
+ expect(identifiedData[0]).toEqual({ data: 0, [DEFAULT_AS]: firstId });
72
+ expect(identifiedData[BLOCK_SIZE]).toEqual({
73
+ data: BLOCK_SIZE,
74
+ [DEFAULT_AS]: firstId + BLOCK_SIZE,
75
+ });
76
+
77
+ // ... and reserve one extra
78
+ expect(identifiedData[BLOCK_SIZE * 2]).toEqual({
79
+ data: BLOCK_SIZE * 2,
80
+ [DEFAULT_AS]: firstId + BLOCK_SIZE * 2,
81
+ });
82
+ });
@@ -0,0 +1,101 @@
1
+ import { asArray } from "../../utils/arrayUtils";
2
+ import { field } from "../../utils/field";
3
+ import FlowNode, { BEHAVIOR_MODIFIES } from "../flowNode";
4
+
5
+ /**
6
+ * @typedef {import("../../spec/transform").LinearizeGenomicCoordinateParams} LinearizeGenomicCoordinateParams
7
+ * @typedef {import("../../view/view").default} View
8
+ */
9
+ export default class LinearizeGenomicCoordinate extends FlowNode {
10
+ get behavior() {
11
+ return BEHAVIOR_MODIFIES;
12
+ }
13
+
14
+ /**
15
+ *
16
+ * @param {LinearizeGenomicCoordinateParams} params
17
+ * @param {View} view
18
+ */
19
+ constructor(params, view) {
20
+ super();
21
+
22
+ const channel = params.channel ?? "x";
23
+
24
+ if (!["x", "y"].includes(channel)) {
25
+ throw new Error("Invalid channel: " + channel);
26
+ }
27
+
28
+ const genome = view.getScaleResolution(channel).getGenome();
29
+ if (!genome) {
30
+ throw new Error(
31
+ "LinearizeGenomicCoordinate transform requires a locus scale!"
32
+ );
33
+ }
34
+
35
+ const chromAccessor = field(params.chrom);
36
+ const posAccessors = asArray(params.pos).map((pos) => field(pos));
37
+ const as = asArray(params.as);
38
+
39
+ if (posAccessors.length != as.length) {
40
+ throw new Error(
41
+ 'The number of "pos" and "as" elements must be equal!'
42
+ );
43
+ }
44
+
45
+ const offsetParam = asArray(params.offset);
46
+
47
+ /** @type {number[]} */
48
+ let posOffsets;
49
+
50
+ if (offsetParam.length == 0) {
51
+ posOffsets = new Array(posAccessors.length).fill(0);
52
+ } else if (offsetParam.length == 1) {
53
+ posOffsets = new Array(posAccessors.length).fill(offsetParam[0]);
54
+ } else if (offsetParam.length == posAccessors.length) {
55
+ posOffsets = offsetParam;
56
+ } else {
57
+ throw new Error(
58
+ `Invalid "offset" parameter: ${JSON.stringify(params.offset)}!`
59
+ );
60
+ }
61
+
62
+ const setter = new Function(
63
+ "datum",
64
+ "chromOffset",
65
+ "posAccessors",
66
+ as
67
+ .map(
68
+ (a, i) =>
69
+ `datum[${JSON.stringify(
70
+ a
71
+ )}] = chromOffset + +posAccessors[${i}](datum) - ${
72
+ posOffsets[i]
73
+ };`
74
+ )
75
+ .join("\n")
76
+ );
77
+
78
+ /** @type {any} */
79
+ let lastChrom;
80
+ let chromOffset = 0;
81
+
82
+ /** @param {string | number} chrom */
83
+ const getChromOffset = (chrom) => {
84
+ if (chrom !== lastChrom) {
85
+ chromOffset = genome.cumulativeChromPositions.get(chrom);
86
+ if (chromOffset === undefined) {
87
+ throw new Error("Unknown chromosome/contig: " + chrom);
88
+ }
89
+ lastChrom = chrom;
90
+ }
91
+
92
+ return chromOffset;
93
+ };
94
+
95
+ /** @param {Record<string, any>} datum */
96
+ this.handle = (datum) => {
97
+ setter(datum, getChromOffset(chromAccessor(datum)), posAccessors);
98
+ this._propagate(datum);
99
+ };
100
+ }
101
+ }
@@ -0,0 +1,44 @@
1
+ import FlowNode, { BEHAVIOR_MODIFIES } from "../flowNode";
2
+ import fontMetadata from "../../fonts/Lato-Regular.json";
3
+ import getMetrics from "../../fonts/bmFontMetrics";
4
+ import { field } from "../../utils/field";
5
+
6
+ /**
7
+ * Measures text length. This is mainly intended for reading-direction arrows
8
+ * in gene annotations.
9
+ *
10
+ * @typedef {import("../../spec/transform").MeasureTextParams} MeasureTextParams
11
+ */
12
+ export default class MeasureTextTransform extends FlowNode {
13
+ get behavior() {
14
+ return BEHAVIOR_MODIFIES;
15
+ }
16
+
17
+ /**
18
+ *
19
+ * @param {MeasureTextParams} config
20
+ */
21
+ constructor(config) {
22
+ super();
23
+
24
+ const metrics = getMetrics(fontMetadata);
25
+ const accessor = field(config.field);
26
+ const as = config.as;
27
+ // TODO: Support custom fonts.
28
+ const size = config.fontSize;
29
+
30
+ /**
31
+ *
32
+ * @param {any} datum
33
+ */
34
+ this.handle = (datum) => {
35
+ const text = accessor(datum);
36
+ if (text !== undefined) {
37
+ datum[as] = metrics.measureWidth(text, size);
38
+ } else {
39
+ datum[as] = 0;
40
+ }
41
+ this._propagate(datum);
42
+ };
43
+ }
44
+ }
@@ -0,0 +1,128 @@
1
+ /* eslint-disable no-unmodified-loop-condition */
2
+ import FlatQueue from "flatqueue";
3
+ import { isNumber } from "vega-util";
4
+ import { field } from "../../utils/field";
5
+ import FlowNode, { BEHAVIOR_MODIFIES } from "../flowNode";
6
+
7
+ const maxDepth = 65536;
8
+
9
+ /**
10
+ * @typedef {import("../../spec/transform").PileupParams} PileupParams
11
+ */
12
+ export default class PileupTransform extends FlowNode {
13
+ get behavior() {
14
+ return BEHAVIOR_MODIFIES;
15
+ }
16
+
17
+ /**
18
+ *
19
+ * @param {PileupParams} params
20
+ */
21
+ constructor(params) {
22
+ super();
23
+
24
+ this.params = params;
25
+ }
26
+
27
+ reset() {
28
+ this.initialize();
29
+ }
30
+
31
+ initialize() {
32
+ const params = this.params;
33
+
34
+ const laneField = params.as || "lane";
35
+ const spacing = isNumber(params.spacing) ? params.spacing : 1;
36
+ const startAccessor = field(params.start);
37
+ const endAccessor = field(params.end);
38
+
39
+ // We choose the implementation based on the need of order preference.
40
+ // The preference-aware algorithm has a lousy O(n^2) time complexity but
41
+ // it's acceptable for finding lanes for genes based on their strands.
42
+
43
+ // Both implementations expect the items to be sorted by their start
44
+ // coordinates.
45
+
46
+ if (!params.preference !== !params.preferredOrder) {
47
+ throw new Error(
48
+ `Must specify both "preference" and "preferredOrder"`
49
+ );
50
+ } else if (params.preference) {
51
+ const freeLaneMap = new Float64Array(maxDepth);
52
+
53
+ const preferenceAccessor = field(params.preference);
54
+ /** @type {any[]} */
55
+ const preferredOrder = params.preferredOrder;
56
+
57
+ let lastStart = Infinity;
58
+
59
+ /** @param {Record<string, any>} datum */
60
+ this.handle = (datum) => {
61
+ const start = startAccessor(datum);
62
+ if (start < lastStart) {
63
+ // Reset if encountered a new chromosome...
64
+ freeLaneMap.fill(-Infinity);
65
+ }
66
+ lastStart = start;
67
+
68
+ // Linear search, but the number of preferences is likely be low
69
+ const preferredLane = preferredOrder.indexOf(
70
+ preferenceAccessor(datum)
71
+ );
72
+ let lane = -1;
73
+ if (preferredLane >= 0 && freeLaneMap[preferredLane] < start) {
74
+ lane = preferredLane;
75
+ } else {
76
+ const start = startAccessor(datum);
77
+ for (lane = 0; lane < freeLaneMap.length; lane++) {
78
+ if (freeLaneMap[lane] < start) {
79
+ break;
80
+ }
81
+ }
82
+ if (lane >= freeLaneMap.length) {
83
+ throw new Error("Out of lanes!");
84
+ }
85
+ }
86
+ freeLaneMap[lane] = endAccessor(datum) + spacing;
87
+ datum[laneField] = lane;
88
+ this._propagate(datum);
89
+ };
90
+ } else {
91
+ /** @type {FlatQueue<number>} */
92
+ const ends = new FlatQueue();
93
+
94
+ /** @type {FlatQueue<number>} */
95
+ const freeLanes = new FlatQueue();
96
+
97
+ // Keep track of the last processed element. Flush the queues if the start
98
+ // pos suddenly decreases. This happens when piling up consecutive chromosomes.
99
+ let lastStart = -Infinity;
100
+
101
+ let maxLane = 0;
102
+
103
+ /** @param {Record<string, any>} datum */
104
+ this.handle = (datum) => {
105
+ const start = startAccessor(datum);
106
+ while (
107
+ ends.length &&
108
+ (ends.peekValue() <= start || start < lastStart)
109
+ ) {
110
+ const freeLane = ends.pop();
111
+ freeLanes.push(freeLane, freeLane);
112
+ }
113
+ lastStart = start;
114
+
115
+ let lane = freeLanes.pop();
116
+ if (lane === undefined) {
117
+ lane = maxLane++;
118
+ }
119
+
120
+ datum[laneField] = lane;
121
+
122
+ this._propagate(datum);
123
+
124
+ ends.push(lane, endAccessor(datum) + spacing);
125
+ };
126
+ }
127
+ }
128
+ }